Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the collect of a flow in a nested fragment (ViewModel) not called?

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.

  • Why am I not able to collect from the shared flow in the nested B1? (When A and B works fine)
  • Am I already not taking some flow rules into consideration? Additional 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?

like image 763
Yokich Avatar asked Sep 06 '25 02:09

Yokich


1 Answers

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)

like image 102
Yokich Avatar answered Sep 09 '25 05:09

Yokich