Room + Kotlin Flow — The modern Android architecture for location-aware apps
If you’ve been developing Android apps for a while, you know how much has changed since the early days of Cupcake. If you want a real flashback, check out the first Android demo that Google published to YouTube back in 2007. It shows maps! Notifications! And a web browser!
Thankfully, things have gotten a lot better for Android users and developers since then. A relatively recent addition to the Android ecosystem is Jetpack, a set of libraries, tools, and best practices to help developers write less code and develop solid, production-worthy apps.
The official guidance from Google on best practices for app architectures is perhaps the most valuable part of this toolkit. Android can get pretty complicated when you stitch a lot of components together, and having recommendations on architecture design from the experts is certainly welcome.
How to handle location updates?
I recently ran into a situation that I couldn’t find a readily-available answer for in the Android Jetpack documentation or code samples — I was looking to re-architect several apps that make heavy use of location updates.
My requirements were:
- Be able to listen for location updates in one place in the app (“single source of truth”) and reflect the results in multiple activities and fragments
- Be able to listen for location updates when the app isn’t visible to the user (e.g., to show updates in a notification and/or log to files while the user is looking at a different app)
#1 is fairly straightforward if you have a single Activity in your application that hosts multiple Fragments, but it gets more complicated if you have multiple Activities (as most Android apps do). #2 is where things really get complicated — in Android, Activities are only active when visible to the user, so we’d need a Service or BroadcastReceiver to receive and process updates.
So what’s the best way to receive location updates in one place and distribute them throughout the app in 2021?
I eventually stumbled on the Google Code Lab for while-in-use-location. This is close to what we want — a Service that listens to location updates and then share the locations with the Activity.
However, there is one problem — the Service uses a
LocalBroadcastManager to broadcast the location to any app component that implements a
As the Android documentation says:
LocalBroadcastManager is an application-wide event bus and embraces layer violations in your app: any component may listen events from any other. You can replace usage of
LocalBroadcastManagerwith other implementation of observable pattern, depending on your use case suitable options may be
LiveDataor reactive streams.
It inherits unnecessary use-case limitations of system
BroadcastManager; developers have to use
Intenteven though objects live in only one process and never leave it. For this same reason, it doesn’t follow feature-wise
So what other implementation of an observable pattern could we use that would be in-line with the recommended Android app architecture?
The solution — Room + Kotlin Flow
After reviewing a number of sources, including the Android “Recommended app architecture” documentation, the repository model seems to be the best design to have an observable single source of truth.
This repository can be backed by several persistent sources of data, with the two modern options being two JetPack components: DataStore and Room. DataStore supports simpler data structures like key-value pairs or protocol buffer-defined objects, but doesn’t support larger, more complex datasets. So, I decided to use Room.
(EDIT August 2021 — Don’t want to add a database like Room to your app? Check out this alternate solution using Kotlin callbackFlow instead!)
So how should we observe changes in Room? For an app written in Kotlin, the canonical Android answer for modern observables is now “Flow”. Kotlin Flow is a native Kotlin feature that allows you to observe a data source and receive updates via a coroutine. And, as of Room 2.2, Room now supports observable reads via Kotlin Flow.
So this is our desired architecture:
To adapt the while-in-use-location example to Room and Flow, we need to change three main things:
- Implement the Room database to persist location data
- Modify the Service to store location updates in Room
- Modify the Activity to observe changes in Room via Flow
The following sections assume some basic knowledge of Room, so you might want to check out the Android Room introduction if you haven’t used it before.
Room implementation 🏠
First, let’s define the new classes that we need to store the location data.
Here’s the model class
Location.kt that contains the Location data, annotated for Room, with a simple identifier that is auto-generated as the primary key, the location timestamp, and the latitude and longitude.
We also need the data access object,
LocationDao.kt, to store and access this information in the database:
Note that the above
updateLocation() function currently deletes previous locations in the database before it inserts the most recent location in order to keep the database size small. If you want to store a history of all locations in the database, you could remove the
The most important part is that
getLocations() returns a list of locations as a Kotlin Flow —
Flow<List<Location>> . We can call this function in our Activity to observe new locations asynchronously as they are being inserted into the database. Locations are ordered by time, so the newest location should always be the last location in the list.
We also need the
LocationDatabase.kt class to define the actual Room database:
…and finally the
LocationRepository.kt, which abstracts the Room implementation as a more general “repository”:
Now we have our data repository— but how do we access this repository in the Activity and Service?
Hilt implementation for dependency injection 🔪
Android Jetpack includes Hilt as a dependency injection framework, which we can use to access the repository from the Activity and Service (and ViewModels, Fragments, or wherever else you need it).
You may have notice the
@Inject annotation in the above
LocationRepository.kt. This tells Hilt that we want to make this component available in other classes.
We also need to annotate the
LocationApplication.kt class to allow Hilt to manage the application:
…and define a
DataModule.kt class that indicates that we want a single, application-wide database to be created (hence the
Now, we can configure the Service to store locations in the repository and Activity to observe updates.
In the Service, we inject the repository using Hilt by annotating the class with
@AndroidEntryPoint and the
@Inject. Then, when we get the new location from Android in
onLocationResult(), we can update the repository with the new location:
Note that we use the KTX lifecycle extension
lifecycleScope to launch the Kotlin coroutine to ensure the coroutine is canceled if the Service is shut down. To use the
lifecycleScope, we need our
ForegroundOnlyLocationService to extend a
LifecycleService instead of a normal
We also use a Kotlin extension function
toLocation() to convert from the system
android.location.Location object to our own
com.example.android.whileinuselocation.model.Location object, which we already defined earlier in this article.
In the Activity, we again inject the repository using Hilt by annotating the class with
@AndroidEntryPoint and the
@Inject. Then, when the activity is starting we observe changes to the database by calling
repository.getLocations(), which returns our
Flow<List<Location>>, and then calling
Now, each time we insert a location in our database in the Service, the
onEach() function will be called by Room with the new result of
repository.getLocations(). In this example, we simply update a
TextView with the new latitude and longitude within the
Note that the above does NOT use LiveData — as this article describes,
Flow.flowWithLifecycle() is the canonical substitution for LiveData in Kotlin Android apps. The coroutine will be scoped to the Activity lifecycle and will only execute if the Activity is at least in the
We can also remove any reference to
BroadcastReceiver in the Activity — because we don’t need it anymore! We’re now using Room + Flow in place of the
Note that we could also use the same architecture within Fragments or ViewModels as well. In fact, this article explains how you can replace LiveData with Flow entirely for Kotlin apps!
Gradle configuration 🔧
No code example would be complete without the configuration needed in
build.gradle to get this to build with Kotlin Flow, Room, and Hilt included as new dependencies.
So, here are the changes needed for the root
…and the application
Above are the library versions I used when implementing the example, but please check when implementing yourself — there are probably new versions by now.
Putting it all together
Here’s a screen capture of the above code in action — you can see how the Service inserts new locations into Room, and the Activity observes them via Kotlin Flow!
That’s it! You now have a fully working example of a modern location-aware architecture for foreground Services in Android using Room and Kotlin Flow, with Hilt injecting the needed dependencies! Go Android Jetpack! 🚀
You can find the above as a working project at https://github.com/barbeau/while-in-use-location in the
room branch in the
complete folder. If you want to see the Git diff with the code changes from the original Google example, click here.
I plan to use this design when updating apps that I maintain (including GPSTest and OneBusAway). The next TODO on my list is figuring out how to handle more complex classes with nested arrays and objects like GnssAntennaInfo and GnssMeasurementEvent. Do you have any suggestions on how to handle them? Please let me know in the comments below.
This is also my first time working with some of these Jetpack components, so if I got something wrong or if you know of a better way to do it, let me know!
If your app doesn’t currently include a database, and you’re concerned with the added overhead, I’d suggest checking out my article on using Kotlin callbackFlow instead of Room!
Finally, I wanted to thank Roar Grønmo for opening the issue flagging that
LocalBroadcastManager was deprecated in the while-in-use-location example, and Chris Arriola for pointing out that
Flow.flowWithLifecycle() can replace LiveData.