Search code examples
androidkotlinnavigationviewmodelkotlin-flow

Sharing a cold flow of MutableStateFlow in ViewModel between two fragments


I've got a problem with a MutableStateFlow property shared between two fragments.

To make it understandable:

I have a BasicViewModel which should be always one instance for both of the fragments because of the nav graph implementation

private val basicViewModel: basicViewModel by navGraphViewModels(R.id.basic_graph) { defaultViewModelProviderFactory }

This ViewModel have a MutableStateFlow property declared like this

    private val _basicProperty = MutableStateFlow<BasicClass?>(null)
    val basicProperty : Flow<BasicClass?> = _basicId
        .filterNotNull()
        .flatMapConcat { someRepository.getBasicProperty(it) }
        .onEach { _basicProperty.value = it }
        .catch {  }

Then, I have FragmentA and FragmentB declared in navigation using nav graphs which calls the property similarly, like this

        basicViewModel.basicProperty
        .filterNotNull()
        .mapNotNull { it.innerProperty}
        .onEach { doSomething(it) }
        .launchIn(viewLifecycleOwner.lifecycleScope)

It all looks fine, but when I navigate to FragmentA the flow of BasicProperty loads (data loads from WebApi) then, I navigate to FragmentB and the flow loads again instead of calling already loaded data which in App it looks kinda lagging because of the reload

Question: What should I do/change to get the already existing data from BasicViewModel in FragmentB?


Solution

  • Your _basicProperty is a hot StateFlow, but you never use it for collecting anything. The property you have exposed, basicProperty, is a cold flow, so each subscriber that collects it will start a new run of the cold flow. Each of these cold flows will be posting their updates to the MutableStateFlow, so at that point, its state is unpredictable because it's showing the latest thing any collector of the shared cold flow is doing.

    I think what you want is for there to be one flow of execution that's shared. So you should have a single StateFlow that is performing the connection, like this:

    val basicProperty : StateFlow<BasicClass?> = _basicId
        .filterNotNull()
        .flatMapConcat { someRepository.getBasicProperty(it) }
        .catch {  }
        .stateIn(viewModelScope, SharingStarted.Eagerly, null)
    

    Your original code doesn't do anything to start the flow until each Fragment comes along to collect it. But this code's call to stateIn starts the flow a single time in the viewModelScope (in this case immediately because of the Eagerly parameter).

    Now this flow only runs once. You can still have each Fragment run its own downstream flow like you were already doing.