Skip to content

refactor: extract stateInUi helper to reduce ViewModel boilerplate#2099

Open
muraki-sports wants to merge 1 commit intoandroid:mainfrom
muraki-sports:feature/add-state-in-ui
Open

refactor: extract stateInUi helper to reduce ViewModel boilerplate#2099
muraki-sports wants to merge 1 commit intoandroid:mainfrom
muraki-sports:feature/add-state-in-ui

Conversation

@muraki-sports
Copy link
Copy Markdown

@muraki-sports muraki-sports commented Apr 16, 2026

Summary

Introduces a stateInUi() extension function in core:ui to eliminate the repetitive stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), ...) pattern used across all ViewModels.

Related prior attempt: #454

Changes

New file

  • core/ui/FlowExtensions.kt — adds context(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-parameters compiler flag (experimental, Kotlin 2.x) required by the context receiver declaration.
  • core/ui/build.gradle.kts — adds androidx.lifecycle.viewModelCompose dependency to resolve viewModelScope inside the extension.

Refactoring

Replaces all stateIn(...) call sites with stateInUi(initialValue = ...) in:

  • appMainActivityViewModel
  • feature/bookmarksBookmarksViewModel
  • feature/foryouForYouViewModel
  • feature/interestsInterestsViewModel
  • feature/searchSearchViewModel
  • feature/settingsSettingsViewModel
  • feature/topicTopicViewModel

Motivation

Every ViewModel repeated the same three-argument stateIn boilerplate. Extracting it into a named helper:

  • removes ~4 lines per call site (net: −75 lines / +71 lines across the codebase)
  • makes the intent clearer — stateInUi signals "this is a UI-scoped state flow"
  • makes future changes to the sharing strategy (timeout, started policy) a single-line fix

@muraki-sports muraki-sports requested a review from dturner as a code owner April 16, 2026 13:09
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 16, 2026

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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread core/ui/build.gradle.kts
api(projects.core.model)

implementation(libs.androidx.browser)
implementation(libs.androidx.lifecycle.viewModelCompose)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

Comment on lines +41 to +46
context(viewModel: ViewModel)
fun <T> Flow<T>.stateInUi(initialValue: T): StateFlow<T> = stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = initialValue,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()
@muraki-sports muraki-sports force-pushed the feature/add-state-in-ui branch from c6d90b5 to 54cdea4 Compare April 16, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant