Notes: 13. Integrate with Kotlin Coroutines
The Kotlin Coroutines homepage provides more insight into what they are and how they work.
SQLDelight provides some extension points for other asynchronous programming models, which connect to its own Query API and the listener structure underneath it to create a better experience for developers.
One of these extensions expresses database functions as Kotlin Flow objects, so if you’re accustomed with the concepts of coroutines and Flow, this lesson will be of interest to you. The SQLDelight team has created an extension library for Kotlin Coroutines which makes working with this asynchronous data delivery much easier.
Let’s add it to our sample app and change one of the screens to use Kotlin Flow instead!
Open the app’s build script and find the addition of the Android driver dependency. Right below it, add a new dependency on the Coroutines Extensions artifact for SQLDelight. Also, we need to add a dependency on the Kotlin Coroutines library itself. After adding both, sync the project to make the new code available.
dependencies {
implementation "com.squareup.sqldelight:coroutines-extensions-jvm:$sqldelightVersion"
// Kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
The Coroutines extension API for SQLDelight consists of only a handful of methods and a single entrypoint.
Whenever you have your hands on a Query object, it’s possible to call ‘asFlow()’ on it to convert it into a Flow of that Query object.
From there, some more extension functions will convert this stream into a list of objects, a single object, or a default value in case the object may be null in the database.
No matter which of these ways you choose for a particular use case, the flow will automatically emit a new value to collectors when the data is being updated - these are all still Flows after all. No need to handle listeners manually anymore - the call sites become very tidy as a result of this.
The collection overview screen shows the list of collections and automatically updates the UI whenever something changes in that list. We will replace the current listener infrastructure that we built into the ViewModel with Kotlin Flow.
Let’s start in the DatabaseRepository and follow the changes down to the UI from there! Open it up and find the listCollections() method. Currently, it returns Query so that the ViewModel can add listeners to the return value.
With Flow, this is provided for us so we don’t need it anymore. Change the return type of the method to Flow<List> - we want to propagate all collections, not just one of them.
-fun listCollections(): Query<Collection> {
+fun listCollections(): Flow<List<Collection>> {
return database.collectionQueries.all()
}
Inside the method, we will continue to call the method ‘all’ of the CollectionQueries object, but now we will add more calls to the end of this chain in order to transform it into a list. Let’s break up this chain into multiple lines to make it easier to follow along.
After the call to ‘all()’, add the extension function ‘asFlow()’ to transform the query into a Flow. At this point in the chain, the return type is Flow, so add another extension function to the end of it in order to get the underlying data.
Write out ‘mapTo’ and check the IDE’s suggestions to see the different options. This is a list query, so we will use ‘mapToList’. Similarly, for the method that only returns a single collection, we could use ‘mapToOne’ instead, for example.
These mapping functions allow you to specify a coroutine context in case you want to switch to a different thread at this point. At this point, the flow’s return type matches and we’re done with the repository.
fun listCollections(): Flow<List<Collection>> {
- return database.collectionQueries.all()
+ return database.collectionQueries
+ .all()
+ .asFlow()
+ .mapToList()
}
Let’s move to the place that calls this method and open the CollectionListViewModel. In here, a bunch of stuff will change now. There is no need for a manual listener anymore, since the Flow will automatically send updates to the data downstream.
Instead, we will start collecting from the flow when the ViewModel is created and bind it to its lifecycle. This will cancel the flow when the screen goes away under the hood.
Let’s get rid of the old stuff first: Delete the listener field and the collectionQuery one as well.
-private val collectionQuery = repository.listCollections()
-
-private val collectionQueryListener = object : Query.Listener {
- override fun queryResultsChanged() {
- refreshState()
- }
-}
Remove the entire contents of the constructor and replace it with a launch block using the available viewModelScope. Inside the block, call the repository’s method to obtain a flow and start collecting it.
init {
- refreshState()
-
- collectionQuery.addListener(collectionQueryListener)
+ viewModelScope.launch {
+ repository.listCollections()
+ .collect { collections ->
+
+ }
+ }
}
Inside the collect lambda, we can update the UI state of this ViewModel directly - there is no need anymore to use the refreshState() method from earlier. Copy its content in here and forward the parameter to the new state like so.
init {
viewModelScope.launch {
repository.listCollections()
.collect { collections ->
+ _state.value = State.Result(
+ collections = collections
+ )
}
}
}
Finally, remove onCleared() and refreshState() altogether - the lifecycle scope will handle cancelation of the flow automatically and we don’t need to worry about it.
-override fun onCleared() {
- collectionQuery.removeListener(collectionQueryListener)
-}
-
-private fun refreshState() {
- _state.value = State.Result(
- collections = collectionQuery.executeAsList()
- )
-}
With the listener handling moved out of the ViewModel and replaced by Kotlin Flow, this call site looks much cleaner than it did with the vanilla API of SQLDelight. The Flow extension library is my favorite way of working with SQLDelight queries, so if your codebase already utilizes coroutines and Flow elsewhere, I’d strongly suggest to look into this one.
As always, hit the Run button to launch the app and watch the screen automatically update the list of collections. As always, it behaves just like before, but now with cleaner code to achieve the same result.