Is Flow LifeCycle aware as LiveData?

Viewed 3498

We know that LiveData is Lifecycle aware and if there's a configuration change LiveData object doesn't re-query from Database (local/remote) each and every time and it gets updated only if there's any update in the data.

Recently I started using Kotlin Flow, I should admit that it's best for the data layer, that is for implementing in the repo so that ViewModel is notified. But I used the Kotlin Flow in ViewModel/View layer also so that I collect the Flow objects in the Fragment directly based on its Status (sealed class implementation). The problem I encountered in this usage of Flow is each and every time when there is a configuration change data is being retrieved from the Database (local/remote).

What should be done in this case? Is there a way to avoid re-querying while using Flow or should I only use LiveData in the ViewModel/View Layer?

Sample Code

sealed class Status<T> {
class Processing<T> : Status<T>()
data class Completed<T>(val value: T) : Status<T>()
data class Error<T>(val error: String) : Status<T>()

companion object {
    fun <T> processing() = Processing<T>()
    fun <T> completed(value: T) = Completed(value)
    fun <T> error(error: String) = Error<T>(error)
    }
}

Repo:

class Repo(database: LocalDatabase){
     fun retrieveUsersData() = flow<Status<List<Users>>>{
    
      emit(Status.processing())

       database.dao.getUsers().collect{
            // Assume db queries successfully and returns list of users
            emit(Status.completed(it))
       }
     }.catch {
            emit(Status.error(it.error.toString()))

     }.flowOn(Dispatchers.IO)
}

ViewModel:

class MyViewModel(application: Application): AndroidViewModel(application) 
{

      private val db = LocalDatabase.getInstance(application)
      private val repo = Repo(db)

      val usersData= repo.retrieveUsersData(dataCollectionType)
}

Fragment:

class UsersFragment: Fragment(){

   override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
   ): View? {
     
    // Assume all the declarations are done properly
      coroutineScope.launch {
          retrieveUsersData()
      }

   }

   private suspend fun retrieveUsersData(){

        viewModel.retrieveUsersData().collect{ status ->
        // Based on the Status do the actions.
       // When Status is Completed set the data to the adapter.
       }
  }

} // end of Fragment
4 Answers

There is actually a asLiveData() to convert Flow to LiveData. You may attach the viewModelScope.coroutineContext to Flow so the Flow will respect the ViewModel's lifecycle. Here is the example based on your code.

class MyViewModel(application: Application): AndroidViewModel(application) 
{

      private val db = LocalDatabase.getInstance(application)
      private val repo = Repo(db)

      val usersData = repo.retrieveUsersData(dataCollectionType).asLiveData(viewModelScope.coroutineContext)
}

Then, observe this LiveData as usual in your view (activity/fragment). When The ViewModel is destroyed, this Flow (converted to LiveData) will be destroy as well.

viewModel.userData.observe(viewLifecycleOwner, { status ->
   // Based on the Status do the actions.
   // When Status is Completed set the data to the adapter.
 }

For best practice,

  • Repo <-> ViewModel -- Flow
  • ViewModel <-> View -- LiveData

It may be a little late , but someone may find this answer useful. If you are using LiveData at the ViewModel, LiveData caches the data on rotation, and the same data will be populated to the Views and therefore there will be no new call to the data source.

Whereas, if you use normal Flow, by default it does not have the caching capability, So on rotation, it will be calling the data source. I guess this is happening in your scenario.

As far as I know, you cannot do this without wrapping the flow from the view model. In your example the ViewModel keeps no real state but merely forwards the flow, so you do not gain any benefit from it surviving the configuration change.

The proposed architecture for your use case is to map the flow to a liveData inside the view model. The video in the link m0skit0 references (https://www.youtube.com/watch?v=B8ppnjGPAGE) shows the usage of the lifeData builder.

In your case that would be as simple as:

class MyViewModel(application: Application): AndroidViewModel(application) 
{
  private val db = LocalDatabase.getInstance(application)
  private val repo = Repo(db)

  val usersData = liveData {
    emit(repo.retrieveUsersData(dataCollectionType))
  }
}

i recently worked on a repo with mvi arch implementation, so based in what you are asking for, i strongly recommend you to collect flow using

addRepeatingJob(Lifecycle.State.STARTED) {
        viewModel.viewState.collect(::render)
    }

render is a function to commute and update UI based on states. Where viewState is a StateFlow<Status<List>>, implement handle data response in viewModel, update ui state based on data retrieving.

You can also add a dummyState to handle hidden view or unexpected close.

Take a look to https://github.com/DwanZ/archMigrationExample/blob/mvi/app/src/main/java/com/example/archmigrationexample/view/home/ui/HomeViewModel.kt

It'll be usefull i hope so.

Related