SharedFlow: Emission is not collected after configuration change

277 views Asked by At

Using Shared ViewModel approach for Activity and its Fragment. Using SharedFlow to share and update UI state in Activity, the change of state is coming from fragments.

Activity (Host)

val viewModel: MainSharedViewModel by viewModels()

// START OF onCreate
with(viewModel) {
   collectShared(mainActivityState, ::onActivityStateChanged)
}
// END OF onCreate

private fun onActivityStateChanged(state: MainActivityState) {

    // Listen which Fragment is visible to user
    if (state is MainActivityState.ToolbarTitleChanged) {

        toolBar.subtitle = state.subTitle

    }

}

Extension Function

inline fun <T : Any, L : SharedFlow<T>> LifecycleOwner.collectShared(
    sharedFlow: L,
    crossinline function: (T) -> Unit,
    lifecycleState: Lifecycle.State = Lifecycle.State.STARTED
) {
    lifecycleScope.launch {
        repeatOnLifecycle(lifecycleState) {
            sharedFlow.collect { t -> function(t) }
        }
    }
}

Fragment

private val mainSharedViewModel: MainSharedViewModel by activityViewModels()

// START OF onCreateView

mainSharedViewModel.setSubTitle() // Empty

// END OF onCreateView

Activity ViewModel

private val _mainActivityState = MutableSharedFlow<MainActivityState>()

val mainActivityState = _mainActivityState.asSharedFlow()

fun setSubTitle(subtitle: String = DEFAULT_VALUE_STRING) {

    viewModelScope.launch {
        _mainActivityState.emit(MainActivityState.ToolbarTitleChanged(subtitle))
    }

}

The above works when switching between fragments via Jetpack Navigation Component, however when a configuration change happens due to switching between Day and Night theme using AppCompatDelegate.setDefaultNightMode. The collector in Activity is not consuming the emission from Fragment.

Upon debugging, we confirmed that the flow is correct

  • Execute Configuration Change via AppCompatDelegate.setDefaultNightMode

  • Activity's collectShared(mainActivityState, ::onActivityStateChanged) triggered

  • Fragment's mainSharedViewModel.setSubTitle() triggered

Unfortunately the Activity's onActivityStateChanged not triggered during this flow. However if I call the mainSharedViewModel.setSubTitle() via button or when I add delay, the collector in activity is receiving the event. I am guessing there is a sort of race condition happening here?

Tried

val mainActivityState = _mainActivityState.asSharedFlow().shareIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L))

and

val mainActivityState = _mainActivityState.asSharedFlow().shareIn(viewModelScope, SharingStarted.Eagerly)

as well as

val mainActivityState = _mainActivityState.asSharedFlow().shareIn(viewModelScope, SharingStarted.Lazily)

but none of them work.

Are we missing something about SharedFlow? Why we are losing the event for this case? I run on this interesting article which might also explain this issue but no idea what changes would be a best course.

3

There are 3 answers

2
Viktor On

As I know SharedFlow doesn't cache the last value by default. Maybe you should try to use StateFlow instead?

2
Jigar Patel On

Here are a few suggestions to handle this situation: ViewModel Store for SharedFlow: Ensure that the SharedFlow is tied to the ViewModel store of the activity. This way, it survives configuration changes, and the emissions will be delivered to the new activity instance.

private val _mainActivityState = MutableSharedFlow<MainActivityState>()

val mainActivityState: SharedFlow<MainActivityState> = _mainActivityState
    .asSharedFlow()
    .shareIn(viewModelStore, SharingStarted.Lazily, replay = 1)

Using viewModelStore as the scope ensures that the SharedFlow is bound to the ViewModel and persists across configuration changes. The replay parameter ensures that the last emission is replayed to new collectors.

Use StateFlow: Consider using StateFlow instead of SharedFlow if you only need to observe the latest state. StateFlow is designed to handle the common use case of sharing state between components.

private val _mainActivityState = MutableStateFlow<MainActivityState>(initialState)

val mainActivityState: StateFlow<MainActivityState> = _mainActivityState

StateFlow is lifecycle-aware and automatically manages subscriptions.

Handle in ViewModel Initialization: Ensure that you initialize the ViewModel and start collecting the SharedFlow early in the activity's lifecycle. It's important to do this before any UI components are set up to avoid missing emissions during configuration changes.

class YourActivity : AppCompatActivity() {

    private val viewModel by viewModels<YourViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        with(viewModel) {
            collectShared(mainActivityState, ::onActivityStateChanged)
        }

        // Other setup code
    }
}
1
Tam Huynh On

I see you call mainSharedViewModel.setSubTitle() in onCreateView, which its lifecycle is just at CREATED state. I assume at this point your Activity may not be in STARTED state yet so the collector has not yet registered.

You should try calling it in onViewCreated, this can make sure the view is fully created and so does the Activity.