I have a setup where an Activity holds two fragments (A, B), and B has a ViewPager with 3 Fragments (B1, B2, B3)
In the activity (ViewModel) I observe a model (Model
) from Room, and publish the results to a local shared flow.
val mSharedFlow = MutableSharedFlow<Model>` that I publish the model updates to:
...
viewModelScope.launch { repo.observeModel().collect(sharedFlow) }
Fragments A and B (ViewModels) have access to the sharedFlow through (Parent) fun getModelFlow(): Flow<Model>
There are no problems running the collects for each Fragment:
viewModelScope.launch {
parent.getModelFlow().collect { model -> doStuff(model) }
}
But, the problem is in the nested fragments (B1 etc.)
In the fragment (ViewModel) for B1 I have another parent.getModelFlow()
that in turn calls Fragment B (ViewModel) parent.getParentFlow()
I have no problem acquiring the flow (i.e the SharedFlow (as Flow from the activity ViewModel)); But the collect
in B1 does nothing.
launch{}'es
, other withContexts(Some?)
, flowOn
, launchIn
etc.?(The providing of the flow is not the problem. Even if I create intermediary flows, or place the sharedFlow in a kotlin Singleton object I still have the same problem)
=== EDIT ===
I was asked to add more information, unfortunately (for all) I can't paste the actual code because it would just appear verbose and foreign (see my comment below). But here's some psuedo-code that should be equivalent.
One clarification, that you can see below, Activity, FragmentA, FragmentB, FragmentB1 (etc.) are all running at the same time- but only one of A and B is visible at one time.
class TheActivity {
fun onCreate() {
setupFragments()
}
/** Two fragments active at the same time,
but only one FrameLayout is visible at one time */
private fun setupFragments() {
val a = FragmentA.newInstance()
val b = FragmentB.newInstance()
supportFragmentManager.commit {
add(R.id.fragment_holder_a, a)
add(R.id.fragment_holder_b, b)
}
}
}
class ActivityViewModel {
val activityModelFlow = MutableSharedFlow<Model>()
fun start() {
viewModelScope.launch {
getRoomFlow(id).collect(activityModelFlow)
}
}
}
class FragmentA { // Not very interesting
val viewModel: ViewModelA
}
class ViewModelA {
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("A model: $model")
}
}
}
}
class FragmentB {
val viewModel: ViewModelB
val viewPagerAdapter = object : PagesAdapter.Pages {
override val count: Int = 1
override fun title(position: Int): String = "B1"
override fun render(position: Int): Fragment = FragmentB1.newInstance()
}
}
class ViewModelB {
val bModelFlow: Flow<Model> get() = parentViewModel.activityModelFlow
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("B model: $model")
}
}
}
}
class Fragment B1 {
val viewModel: ViewModelB1
}
class ViewModelB1 {
fun start() {
viewModelScope.launch {
// in essence:
// - ViewModelB.bModelFlow ->
// - ActivityViewModel.modelFlow
parentViewModel.bModelFlow.collect { model ->
log("B1 model: $model")
}
}
}
}
So, all of the connections of acquiring parentViewModels, DI, fragment creation etc. is all working fine. But B1 model: $model
is never called! Why?
This had very little (read no) connection to fragments, lifecycle, flows and coroutines blocking - which I thought was behind this.
val activityModelFlow = MutableSharedFlow<Model>()
// is the same as
val activityModelFlow = MutableSharedFlow<Model>(
replay = 0,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
This means that new subscribers will get access to 0
(!) of the values that are stored in the replay cache. So when FragmentB1 gets around to subscribing, the model has already been emitted.
Solution (without any optimisation or further consideration)
private val bulletFlow = MutableSharedFlow<Bullet>(replay = 1)
(or use a StateFlow, but I don't want to bother with initial state/value)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With