Kotlin Flow for Android: Getting Started
In this tutorial, you’ll learn about the basics of Kotlin Flow, and you’ll build an Android app that fetches weather forecast data using Flow. By Dean Djermanović.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Flow for Android: Getting Started
30 mins
Flow on Android
Now you'll apply everything you've learned so far in an Android app! The Sunzoid app is a simple weather app that displays a forecast for a specific city. It fetches the weather data from the network and stores it into a database to support offline mode.
Open the Sunzoid-Starter project in Android Studio. Build and run the app, and you'll see an empty screen:
There's a search icon in the top left corner. You can tap it to enter a specific location. If you do that now, nothing will happen. But hang on — you're going to implement this functionality next!
There's a fair amount of code in the starter project:
You'll focus on the use of Kotlin Flow in the app. But if you want, you can explore the code, and get familiar with the app!
The starter project follows Google's recommended guide to app architecture. You can find the guide on the Android developer site documentation:
At the top of the scheme, there's a UI layer that talks to the ViewModel architecture component. ViewModel communicates with a data repository. The repository fetches the data from the network using Retrofit. It stores the data in a local Room database. Finally, it exposes the database data to the ViewModel.
Room and Retrofit, in their latest versions, support Kotlin Coroutines. The starter project is set up to use them with coroutines.
You'll use Kotlin Flow to pass the data from the database to the ViewModel. The ViewModel will then collect the data. You'll also use coroutines and Flow to implement the search functionality.
Fetch Data
You'll start by implementing the logic to fetch the forecast data. Open HomeActivity.kt. In onCreate()
, add a call to fetchLocationDetails()
, right below initUi()
:
homeViewModel.fetchLocationDetails(851128)
fetchLocationDetails()
accepts a cityId
as an argument. For now, you'll pass the hardcoded ID. You'll add a search feature later that will allow you to search for a specific location.
Build and run the project. You still won't see anything on the screen:
But this time the app has fetched the forecast data and saved it to the Room database! :]
Room and Flow
In Room 2.1, the library added coroutine support for one-off operations. Room 2.2 added Flow support for observable queries. This enables you to get notified any time you add or remove entries in the database.
In the current implementation, only the user can trigger data fetching. But you can easily implement logic that schedules and updates the database every three hours, for example. By doing this, you make sure your UI is up to date with the latest data. You'll use Kotlin Flow to get notified of every change in the table.
Plugging Into the Database
Open ForecastDao.kt and add a call to getForecasts()
. This method returns Flow<List<DbForecast>>
:
@Query("SELECT * FROM forecasts_table")
fun getForecasts(): Flow<List<DbForecast>>
getForecasts()
returns forecast data for a specific city from forecasts_table
. Whenever data in this table changes, the query executes again and Flow
emits fresh data.
Next, open WeatherRepository.kt and add a function called getForecasts
:
fun getForecasts(): Flow<List<Forecast>>
Next, add the implementation to WeatherRepositoryImpl.kt:
override fun getForecasts() =
forecastDao
.getForecasts()
.map { dbMapper.mapDbForecastsToDomain(it) }
This method uses the forecastDao
to get data from the database. The database returns the database model. It's a good practice for every layer in the app to work with its own model. Using map()
, you convert the database model to the Forecast
domain model.
Open HomeViewModel.kt and add forecasts
, like so:
//1
val forecasts: LiveData<List<ForecastViewState>> = weatherRepository
//2
.getForecasts()
//3
.map {
homeViewStateMapper.mapForecastsToViewState(it)
}
//4
.asLiveData()
There are a few things going on here:
- First, you declare
forecasts
of theLiveData<List<ForecastViewState>>
type. TheActivity
will observe changes inforecasts
.forecasts
could have been of theFlow<List<ForecastViewState>>
type, butLiveData
is preferred when implementing communication between View and ViewModel. This is becauseLiveData
has internal lifecycle handling! - Next, reference
weatherRepository
to get the Flow of forecast data. - Then call
map()
.map()
converts the domain models to theForecastViewState
model, which is ready for rendering. - Finally, convert a
Flow
toLiveData
, usingasLiveData()
. This function is from the AndroidX KTX library forLifecycle
andLiveData
.
Context Preservation and Backpressure
The collection of a Flow always happens in the context of the parent coroutine. This property of Flow is called context preservation. But you can still change the context when emitting items. To change the context of emissions you can use flowOn()
.
You could have a scenario in which the Flow produces events faster than the collector can consume them. In reactive streams, this is called backpressure. Kotlin Flow supports backpressure out of the box since it's based on coroutines. When the consumer is in a suspended state or is busy doing some work, the producer will recognize that. It will not produce any items during this time.
Observing Values
Finally, open HomeActivity.kt and observe forecasts
from initObservers()
:
homeViewModel.forecasts.observe(this, Observer {
forecastAdapter.setData(it)
})
Whenever forecasts change in the database, you'll receive new data in the Observer, and display it on the UI.
Build and run the app. Now the home screen displays forecast data! :]
Congratulations! You've implemented communication between multiple layers of your app using Flow
and LiveData
!
Cancellation
In HomeViewModel.kt, you're observing the forecasts
. You've noticed that you never stop observing. How long is this observed, then?
In this case, the Flow collection starts when LiveData
becomes active. Then, if LiveData
becomes inactive before the Flow completes, the flow collection is canceled.
The cancellation occurs after a timed delay unless LiveData
becomes active again before that timeout. The default delay triggering cancellation is 5000 milliseconds. You can customize the timeout value if necessary. The timeout exists to handle cases like Android configuration changes.
If LiveData
becomes active again after cancellation, the Flow collection restarts.
Exceptions
Flow streams can complete with an exception if an emitter or code inside the operators throws an exception. catch()
blocks handle exceptions within Flows. You can do this imperatively or declaratively. A try-catch
block on the collector's side is an example of an imperative approach.
It's imperative because these catch any exceptions that occur in the emitter or in any of the operators.
You can use catch()
to handle errors declaratively instead. Declarative here means you declare the function to handle errors. And you declare it within the Flow itself, and not a try-catch
block.
Open HomeViewModel.kt and navigate to forecasts
. Add catch()
right before map()
. To simulate errors in the stream, throw an exception from map()
:
val forecasts: LiveData<List<ForecastViewState>> = weatherRepository
.getForecasts()
.catch {
// Log Error
}
.map {
homeViewStateMapper.mapForecastsToViewState(it)
throw Exception()
}
.asLiveData()
Build and run the app. You'll notice that the app crashes! catch()
catches only upstream exceptions. That is, it catches exceptions from all the operators above the catch. catch()
doesn't catch any exception that occurs after the operator.
Now move catch()
below map()
:
val forecasts: LiveData<List<ForecastViewState>> = weatherRepository
.getForecasts()
.map {
homeViewStateMapper.mapForecastsToViewState(it)
throw Exception()
}
.catch {
// Log Error
}
.asLiveData()
Build and run the app again. Now you'll see an empty screen:
This is an example of exception transparency, where you're able to separate the handling of exceptions that occur in the Flow from the collection of values. You're also being transparent about exceptions, as you don't hide any errors, you explicitly handle them in an operator!
Before proceeding, remove the line that throws an exception from map()
.