Keeping the Changes Flowing

At this point, your database has been fully set up and connected to your UI. However, your current code is only suited to query the database once. Eventually, you will be able to add and update individual crimes – but at the moment, if other parts of your app tried to update the database, CrimeListFragment would be oblivious to the changes and would happily present stale data.

While you could add code to manually reconcile updates from specific parts of your app, it would be better to observe the database so that CrimeListFragment automatically receives all updates to the database, regardless of where they come from.

Which brings us back to coroutines, along with two new classes: Flow and StateFlow.

Built into the Coroutines library, a flow represents an asynchronous stream of data. Throughout their lifetime, flows emit a sequence of values over an indefinite period of time that get sent to a collector. The collector will observe the flow and will be notified every time a new value is emitted in the flow.

Flows are a great tool for observing changes to a database. In a moment, you will create a flow that contains all the Crime objects in your database. If a crime is added, removed, or updated, the flow will automatically emit the updated set of crimes to its collectors, keeping them in sync with the database. This all ties in nicely with the end goal of this chapter: to have CrimeListFragment display the freshest data from your database.

Refactoring your code to use a Flow will touch a handful of files in your project:

  • your CrimeDao, to make it emit a flow of crimes

  • your CrimeRepository, to pass that flow of crimes along

  • your CrimeListViewModel, to get rid of loadCrimes() and instead present that flow of crimes in an efficient way to its consumers

  • your CrimeListFragment, to collect the crimes from the flow and update its UI

You will start making your changes at the database level and work up the layers until you get to your CrimeListFragment.

Refactoring the database to provide you with a Flow of crimes is relatively straightforward. Room has built-in support to query a database and receive the results in a Flow.

Since you are not making any changes to the structure of the database, you do not need to make any changes in the Crime and CrimeDatabase classes. In CrimeDao, update getCrimes() to return a Flow<List<Crime>> instead of a List<Crime>. Also, you do not need a coroutine scope to handle a reference to a Flow, so remove the suspend modifier. (You will need a coroutine scope when trying to read from the stream of values within the Flow, but you will handle that in just a second.)

Listing 12.24  Creating a Flow from your database (CrimeDao.kt)

@Dao
interface CrimeDao {
    @Query("SELECT * FROM crime")
    suspend fun getCrimes(): List<Crime> Flow<List<Crime>>

    @Query("SELECT * FROM crime WHERE id=(:id)")
    suspend fun getCrime(id: UUID): Crime
}

Make sure you import the kotlinx.coroutines.flow version of Flow.

Since you access the CrimeDatabase through the CrimeRepository, make the same changes there:

Listing 12.25  Refactoring a level higher (CrimeRepository.kt)

class CrimeRepository private constructor(context: Context) {
    ...
    suspend fun getCrimes(): List<Crime> Flow<List<Crime>>
            = database.crimeDao().getCrimes()
    ...
}

Next, clean up your CrimeListViewModel. You will no longer be using the loadCrimes() function, and you can get rid of the logging statements. Also, update the crimes property to pass the Flow along.

Listing 12.26  Clearing the slate (CrimeListViewModel.kt)

class CrimeListViewModel : ViewModel() {
    private val crimeRepository = CrimeRepository.get()

    val crimes = mutableListOf<Crime>() crimeRepository.getCrimes()

    init {
        Log.d(TAG, "init starting")
        viewModelScope.launch {
            Log.d(TAG, "coroutine launched")
            crimes += loadCrimes()
            Log.d(TAG, "Loading crimes finished")
        }
    }

    suspend fun loadCrimes(): List<Crime> {
        return crimeRepository.getCrimes()
    }
}

You have reached the layer where you display UI. To access the values within the Flow, you must observe it using the collect {} function.

collect {} is a suspending function, so you need to call it within a coroutine scope. Thankfully, you already set up a coroutine scope within CrimeListFragment’s onViewCreated() callback.

In that callback, replace your call to loadCrimes() (whose definition you just deleted) with a collect {} function call on the crimes property from CrimeListViewModel. The lambda you pass into the collect {} function will be invoked every time there is a new value in the Flow, so that is the perfect place to set the adapter on your RecyclerView.

Listing 12.27  Collecting your StateFlow from CrimeListFragment (CrimeListFragment.kt)

class CrimeListFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                val crimes = crimeListViewModel.loadCrimes()
                crimeListViewModel.crimes.collect { crimes ->
                    binding.crimeRecyclerView.adapter =
                        CrimeListAdapter(crimes)
                }
            }
        }
    }
    ...
}

(Make sure you import kotlinx.coroutines.flow.collect.)

Compile and run the app. Once again, you should see the prepopulated crimes from the database. However, you are not done. If you rotate your device and initiate a configuration change, you might notice a brief moment when the screen is blank, waiting to load the crimes from the database. You are still performing a new database query on each configuration change.

Earlier in this chapter, we talked about how you can use a ViewModel to perform expensive calculations and cache results across configuration changes. Every time you collect values from the crimes property within your CrimeListFragment, you are creating a new Flow and performing a new database query for that Flow. That is an inefficient use of resources, and if your database query takes a long time to execute, your users will see a blank screen while the data is being loaded.

It would be better to maintain a single stream of data from your database and cache the results so they can quickly be displayed to the user. And that is where StateFlow comes in.

StateFlow is a specialized version of Flow that is designed specifically to share application state. StateFlow always has a value that observers can collect from its stream. It starts with an initial value and caches the latest value that was emitted into the stream. It is the perfect companion to the ViewModel class, because a StateFlow will always have a value to provide to fragments and activities as they get re-created.

The first step in setting up a StateFlow is to create an instance of a MutableStateFlow. Analogous to List and MutableList, StateFlow is a read-only Flow while MutableStateFlow allows you to update the value within the stream. When creating a MutableStateFlow, you must provide an initial value, so in this situation you will provide an empty list. This is the value that collectors will receive before any other values are put in the stream.

Using a viewModelScope in the init block of your CrimeListViewModel, you can collect values from your CrimeRepository. Once you have your value from the database Flow, you can set the value on your MutableStateFlow.

To keep your code maintainable and the stream of data flowing in one direction from the database all the way to the UI, you need to be careful about how you provide your data to consumers. If you provide your data in the form of a MutableStateFlow, then you are giving the fragments and activities that collect from it the ability to put values directly into the stream. Normally, you want to protect access to the stream, so it is a common practice to keep your MutableStateFlow private to the class and only expose it to collectors as a read-only StateFlow.

Add the following code to implement your StateFlow in CrimeListViewModel.kt:

Listing 12.28  Efficiently caching the database results (CrimeListViewModel.kt)

class CrimeListViewModel : ViewModel() {
    private val crimeRepository = CrimeRepository.get()

    val crimes = crimeRepository.getCrimes()
    private val _crimes: MutableStateFlow<List<Crime>> = MutableStateFlow(emptyList())
    val crimes: StateFlow<List<Crime>>
        get() = _crimes.asStateFlow()

    init {
        viewModelScope.launch {
            crimeRepository.getCrimes().collect {
                _crimes.value = it
            }
        }
    }
}

Run the app again to make sure everything works as expected. Now, you efficiently access the data within your database, and your UI will always display the latest data. In the next chapter, you will connect the crime list and crime detail screens and populate the crime detail screen with data for the crime you click in the database.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset