Home > other >  Flow re-collecting using repeatOnLifecycle API
Flow re-collecting using repeatOnLifecycle API

Time:01-09

When I collect a StateFlow in an Activity/Fragment using repeatOnLifecycle and then navigate to another activity then go back to the base one then the flow is re-collecting even if I don't update stateFlow.

For example:

in ViewModel

private var _deletionStatusStateFlow = MutableStateFlow(0)
val deletionStatusStateFlow = _deletionStatusStateFlow.asStateFlow()

then i observed it in Fragment:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){

         deleteAccountViewModel.deletionStatusStateFlow.collect {

           if (it == 1){
             startActivity(AnyActivity)
           }
         }
     }
}

it keeps open the activity every time I click onBackKey

but if i use LiveData with same example ... the observing block will not execute again (when coming back to the STATRED state in the Fragment -view- )

How do I achieve similar behavior to LiveData in StateFlow?

There's a solution for simple usage: it's when I use flowWithLifecycle(...).distinctUntilChanged() but this is complex:

val results: StateFlow<SearchResult> =
    queryFlow.mapLatest { query ->
        repository.search(query)
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000L),
        initialValue = SearchResult.EMPTY
    )

the up stream will be re-generated (and that will cost an upstream repo read)

CodePudding user response:

This is expected behavior because Activities and Fragments can be recreated multiple times. That’s why repeatOnLifecycle exists in the first place.

You need to wrap your data in a class that also has a Boolean property indicating whether the associated navigation event has occurred. The collector in the Activity/Fragment, when it performs the navigation event, should also call a ViewModel function that the ViewModel uses to update the Flow to emit an event where the navigation event is considered handled. This is too complicated for stateIn. You’ll want to use a MutableStateFlow so you can update the values based on the feedback from the Activity.

This process is described in the Android documentation here.

CodePudding user response:

Navigation is often best represented by a regular Flow (or a SharedFlow), rather than a StateFlow. A navigation event should only be consumed once by collectors, and then discarded, as opposed to UI state which can be re-read as much as you like. If you do not discard the event (a Flow does this for you) you will get the behaviour you are seeing, where the navigation is triggered multiple times.

Considering this, I would expose navigation as a Flow from your view model, separate to any UI state flow you might have.

View Model

// Define our UI state flow
private val _stateFlow = MutableStateFlow(initialState) // Side note: this mutable flow does not need to use "var", we can and should use "val"
val stateFlow: StateFlow<UiState> = _stateFlow.asStateFlow()

// Define our navigation event flow
val navigationFlow: Flow<NavigationEvent> = ... // Logic to construct navigation flow goes here.
// You might use flow { ... } or a MutableSharedFlow<NavigationEvent> depending on your needs, but you only need to expose a Flow.

If you have more than one type of navigation event for this view model, you'll want to have them clearly defined, like this.

Models

sealed interface NavigationEvent {
    object GoToXyzActivity : NavigationEvent
}

Now we'll collect the flows we've exposed from the view model in the Activity.

Activity

// Collect our UI state
viewLifecycleOwner.lifecycleScope.launch {
    deleteAccountViewModel.stateFlow.collect {    
      
  viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Update the view based on the UI state here
        }
    }
}

// Collect our navigation events
viewLifecycleOwner.lifecycleScope.launch {
    // If your navigation doesn't interact with the activity's view,
    // we actually don't need to use `repeatOnLifecycle()`, since `startActivity()`
    // does not required the view to exist to work - we could be in
    // any stage of the activity's `Lifecycle` and that's fine.
    deleteAccountViewModel.navigationFlow.collect { navEvent ->
        when (navEvent) {
            GoToXyzActivity -> startActivity(XyzActivity)
            // Other navigation can go here
        }
    }
}

Just an FYI, this is from the Kotlin docs on StateFlows

Unlike a cold flow built using the flow builder, a StateFlow is hot: collecting from the flow doesn't trigger any producer code.

This means an upstream call to your repo won't be re-triggered as long as you're storing the result in a StateFlow in your view model somewhere.

  • Related