-
Notifications
You must be signed in to change notification settings - Fork 113
Alter viewmodel-sample to show how to use a not always hot StateFlow #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from 1 commit
4e80877
e9dcfda
1ec6561
8c1621b
395125d
19bd668
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.example.molecule.viewmodel | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import kotlinx.coroutines.flow.Flow | ||
|
|
||
| interface MoleculePresenter<Event, Model> { | ||
| val seed: Model | ||
|
|
||
| @Composable | ||
| fun present(events: Flow<Event>): Model | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,16 +15,20 @@ | |
| */ | ||
| package com.example.molecule.viewmodel | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.platform.AndroidUiDispatcher | ||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import app.cash.molecule.RecompositionMode.ContextClock | ||
| import app.cash.molecule.launchMolecule | ||
| import app.cash.molecule.moleculeFlow | ||
| import kotlin.time.Duration.Companion.seconds | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.SharingStarted | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.WhileSubscribed | ||
| import kotlinx.coroutines.flow.onEach | ||
| import kotlinx.coroutines.flow.stateIn | ||
|
|
||
| abstract class MoleculeViewModel<Event, Model> : ViewModel() { | ||
| private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) | ||
|
|
@@ -34,9 +38,16 @@ abstract class MoleculeViewModel<Event, Model> : ViewModel() { | |
| private val events = MutableSharedFlow<Event>(extraBufferCapacity = 20) | ||
|
|
||
| val models: StateFlow<Model> by lazy(LazyThreadSafetyMode.NONE) { | ||
| scope.launchMolecule(mode = ContextClock) { | ||
| models(events) | ||
| } | ||
| moleculeFlow(mode = ContextClock) { | ||
| val presenter = remember { presenterFactory() } | ||
| presenter.present(events) | ||
| }.onEach { | ||
| seed = it | ||
| }.stateIn( | ||
| scope = scope, | ||
| started = SharingStarted.WhileSubscribed(5.seconds), | ||
| initialValue = seed, | ||
|
Comment on lines
+48
to
+55
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole pattern really feels like it should be encapsulated in something. I could see it being one (or more) of the following:
I legitimately cannot figure out what to do with this PR. The use case is certainly valid. But the amount of code required to accomplish it as-is seems like it would overwhelm someone trying to learn how to use Molecule with AndroidX View Model rather than guide them.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is not really a reflection of your PR, more of the current library design, the design of View Model, and the general sadness of lifecycle.)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes I understand and I agree. It really shouldn't be this way, but it is the current reality unfortunately.
So my entire reasoning behind starting this PR came down to the steps we took to adopt molecule in the app I work for: We were looking into adopting Molecule for our codebase for some time now.
Basically then, I fear that people who use AAC ViewModel will go up to step # 2 here, and then stop there without realizing there's more to be done. Then eventually when they figure out what happens, they'll blame Molecule instead of ViewModel since that's when this change would have been introduced to them. If we can do something to make this PR even better and make the code more understandable, perhaps from the points you suggest I'd love for that to happen of course. Other than that I personally still feel like it's a net positive if the sample which a lot of people are probably going to follow blindly is actually playing well with AAC ViewModel.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JakeWharton, if we consider On reflection, I think the happy places to land are likely to be:
The last two are most appealing to me. But even And is keeping it running really so bad? How many would actually be kept running at one time in the VM framework? Is it just the VMs currently on screen during config change, or have I fallen off the boat here?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's all the VMs in screens in the backstack, so potentially many of them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worth noting that a The recommended period for state production to be halted in the absence observers with ViewModels is 5 seconds. If a screen is in the back stack for 5 seconds, it should still have access to it's last produced non parcelable state when resumed IMO. Otherwise, leaving a screen for > 5 seconds and returning to it will have the same behavior as the app cold starting on that screen. In the case of a screen with a paginated list for example, this may mean a progress bar till the list is refreshed and the scroll position properly restored.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep. |
||
| ) | ||
| } | ||
|
|
||
| fun take(event: Event) { | ||
|
|
@@ -45,6 +56,21 @@ abstract class MoleculeViewModel<Event, Model> : ViewModel() { | |
| } | ||
| } | ||
|
|
||
| @Composable | ||
| protected abstract fun models(events: Flow<Event>): Model | ||
|
StylianosGakis marked this conversation as resolved.
|
||
| /** | ||
| * This value serves as the initial value that the uiState [StateFlow] will emit and then as a | ||
| * way to cache the last emission. | ||
| * When the flow goes from being cold (when in the backstack and it has no observers) to being | ||
| * hot again, by default the value cached using [stateIn] will be overwritten by the Presenter's | ||
| * first emission. By default the presenter at that point won't have any notion of what that | ||
| * cached value was without us providing this seed [Model]. | ||
| * It's the responsibility of the consumer to actually use this seed value when creating the | ||
| * Presenter inside the [presenterFactory]. | ||
| */ | ||
| abstract var seed: Model | ||
|
|
||
| /** | ||
| * This will be remembered in the context of the moleculeFlow, so that it stays alive for as long | ||
| * as the [models] [StateFlow] is still hot (has observers or the timeout hasn't timed out yet). | ||
| */ | ||
| protected abstract fun presenterFactory(): MoleculePresenter<Event, Model> | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The need for this
initialValuedoes make the case that having astartedparameter onlaunchMoleculeis maybe not the worst idea in the world. Hooking it up with the existing API surface is uhh waves hands possible, for a certain definition of possible, but not one useful to someone who wants to do whatMoleculeViewModelis doing