refactor: extract stateInUi helper to reduce ViewModel boilerplate#2099
refactor: extract stateInUi helper to reduce ViewModel boilerplate#2099muraki-sports wants to merge 1 commit intoandroid:mainfrom
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a stateInUi extension function in the core:ui module to standardize the conversion of Flow to StateFlow within ViewModels, leveraging the experimental Kotlin context parameters feature. This utility simplifies code across multiple ViewModels by encapsulating the common WhileSubscribed sharing strategy. Review feedback recommends changing the ViewModel dependency from implementation to api to prevent compilation issues in consumer modules and suggests improving the extension function's flexibility by parameterizing the sharing policy and extracting magic numbers.
| api(projects.core.model) | ||
|
|
||
| implementation(libs.androidx.browser) | ||
| implementation(libs.androidx.lifecycle.viewModelCompose) |
There was a problem hiding this comment.
Since ViewModel is used in the public signature of the stateInUi extension function (as a context parameter), this dependency should be declared as api instead of implementation. This ensures that consumer modules have access to the ViewModel type during compilation when they use this extension.
api(libs.androidx.lifecycle.viewModelCompose)| context(viewModel: ViewModel) | ||
| fun <T> Flow<T>.stateInUi(initialValue: T): StateFlow<T> = stateIn( | ||
| scope = viewModel.viewModelScope, | ||
| started = SharingStarted.WhileSubscribed(5_000), | ||
| initialValue = initialValue, | ||
| ) |
There was a problem hiding this comment.
Consider extracting the 5_000 timeout into a named constant to avoid magic numbers and improve maintainability. Additionally, making the SharingStarted policy an optional parameter provides flexibility for ViewModels that might require a different sharing strategy (e.g., Eagerly or a different timeout) while keeping the common UI pattern concise.
private const val StopTimeoutMillis = 5_000L
context(viewModel: ViewModel)
fun <T> Flow<T>.stateInUi(
initialValue: T,
started: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis),
): StateFlow<T> = stateIn(
scope = viewModel.viewModelScope,
started = started,
initialValue = initialValue,
)Add FlowExtensions.kt in core:ui with a context(viewModel: ViewModel) stateInUi() extension that wraps the standard stateIn() pattern used across all ViewModels. - core/ui: add FlowExtensions.kt with stateInUi() - core/ui: add lifecycle-viewmodel-compose dependency - build-logic: enable -Xcontext-parameters compiler flag - app, feature/*: replace stateIn(...) calls with stateInUi()
c6d90b5 to
54cdea4
Compare
Summary
Introduces a
stateInUi()extension function incore:uito eliminate the repetitivestateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), ...)pattern used across all ViewModels.Related prior attempt: #454
Changes
New file
core/ui/FlowExtensions.kt— addscontext(viewModel: ViewModel) fun <T> Flow<T>.stateInUi(initialValue: T): StateFlow<T>, a shorthand for the standard UI state flow pattern.Build
build-logic/KotlinAndroid.kt— enables the-Xcontext-parameterscompiler flag (experimental, Kotlin 2.x) required by the context receiver declaration.core/ui/build.gradle.kts— addsandroidx.lifecycle.viewModelComposedependency to resolveviewModelScopeinside the extension.Refactoring
Replaces all
stateIn(...)call sites withstateInUi(initialValue = ...)in:app—MainActivityViewModelfeature/bookmarks—BookmarksViewModelfeature/foryou—ForYouViewModelfeature/interests—InterestsViewModelfeature/search—SearchViewModelfeature/settings—SettingsViewModelfeature/topic—TopicViewModelMotivation
Every ViewModel repeated the same three-argument
stateInboilerplate. Extracting it into a named helper:stateInUisignals "this is a UI-scoped state flow"