One-time suspending data fetches from data sources using the execute
method in your ViewModels is a simple way to populate your screens with data.
However, there are cases where this isn’t quite enough:
A neat way to do this is by using coroutine Flows.
First of all, you’d have to create a Flow
somewhere in one of the Data sources of your application, to expose data from it. If you need to do this manually, callbackFlow
will often be the way to go.
However, there are also libraries that already support the Flow
type, notably, Room. If you return a type wrapped in a Flow
from your Room queries, it will re-emit the results into that Flow
when the contents of the database table change.
The example shown here is from the GuardianBusinessNews sample app.
@Dao
interface NewsDao {
@Query("SELECT * FROM newsitems")
fun getAllNewsItems(): Flow<List<RoomNewsItem>>
}
Note that a method querying for a Flow
of data doesn’t have to be (and shouldn’t be) a suspending method. Methods that return a Flow
will return immediately.
From here, you can return the Flow from your Data source, your Interactor, and your Presenter as well.
If you need to perform operations on the Flow of data, you can add those at any layer. For example, you can transform database specific models into domain models in your Data source, and domain models to screen specific presentation models.
class DiskDataSource @Inject constructor(
private val newsDao: NewsDao
) {
fun getSavedNews(): Flow<List<News>> {
return newsDao.getAllNewsItems().map { news -> news.map(RoomNewsItem::toNews) }
}
}
For more about mapping in RainbowCake, see Theory: Models and Mapping code style.
If you do perform operations on the Flow along the way, you’ll want to make sure that you’re doing it on the correct thread. Since the are no suspending calls here, you don’t have to call withIOContext
in your Presenter. However, that’s a good place to specify where the Flow should… flow.
This can be done with the flowOn
method, which will make sure that any operations called on the Flow upstream are executed on a background thread:
class SavedPresenter @Inject constructor(
private val newsInteractor: NewsInteractor
) {
fun getSavedNews(): Flow<List<SavedNewsItem>> {
return newsInteractor.getSavedNews()
.map { news ->
news.map(News::toSavedNewsItem)
}
.flowOn(Dispatchers.IO)
}
}
A note about testability: in the future, RainbowCake will likely provide a utility method for flowing in the IO context, which will also have support in the rainbow-cake-test
artifact. For now, if you’re using flowOn
and want to test your Presenters, you’ll have to inject a Dispatcher yourself, so that you can replace the one being used during tests.
The previous section covered getting a Flow all the way to the ViewModel, let’s see how these values can be collected and put to use.
In the ViewModel, you can use collect
to process the values of the Flow, which is a suspending function. This means that you have to call it from inside a coroutine.
The most important thing to remember here is that collect
will suspend until the entire Flow is collected. This means that collecting values inside an execute
block will prevent any other actions from being processed, until the Flow
ends (if it ever does).
This means that you should use the non-blocking executeNonBlocking
method instead. If you want to get updates from the Flow
for the entire lifetime of a ViewModel, you can do this in an init
block, like so:
class SavedViewModel @Inject constructor(
private val savedPresenter: SavedPresenter
) : RainbowCakeViewModel<SavedViewState>(Loading) {
init {
executeNonBlocking {
savedPresenter.getSavedNews().collect { news ->
viewState = if (news.isEmpty()) {
Empty
} else {
SavedReady(news)
}
}
}
}
}
Inside the collect
call, it’s up to you to decide how to react to the values you receive. You can make updates to the ViewState
, or send events to the View layer. The code inside collect
is executed on the UI thread.
Here’s some more recommended reading on Flows: