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!

The first Android demo video from Google

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?

My requirements were:

  1. 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
  2. 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?

Almost there…

However, there is one problem — the Service uses a LocalBroadcastManager to broadcast the location to any app component that implements a BroadcastReceiver.

The “event bus” design of LocalBroadcastManager is deprecated because it violates Android architecture principles

LocalBroadcastManager has been deprecated because it violates several principles in the JetPack “Guide to app architecture”.

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 LocalBroadcastManager with other implementation of observable pattern, depending on your use case suitable options may be LiveData or reactive streams.

It inherits unnecessary use-case limitations of system BroadcastManager; developers have to use Intent even though objects live in only one process and never leave it. For this same reason, it doesn’t follow feature-wise BroadcastManager.

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

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.

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:

Our UI can observe changes from the repository (Room) using Kotlin Flow

To adapt the while-in-use-location example to Room and Flow, we need to change three main things:

  1. Implement the Room database to persist location data
  2. Modify the Service to store location updates in Room
  3. 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 🏠

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 deleteLocations() line.

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 🔪

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 @InstallIn(SingletonComponent::class) and @Singleton annotations):

Now, we can configure the Service to store locations in the repository and Activity to observe updates.

Service

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 Service.

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.

Activity

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 logResultsToScreen() function.

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 STARTED state.

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 LocalBroadcastManager!

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 🔧

So, here are the changes needed for the root build.gradle:

…and the application build.gradle:

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

The Service inserts new locations into Room, and the Activity observes them via Kotlin Flow!

Closing thoughts

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!

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.

Was this article helpful? Consider following me on Medium. You can also find me on LinkedIn and Twitter.

Improving the world, one byte at a time. @sjbarbeau, https://github.com/barbeau, https://www.linkedin.com/in/seanbarbeau/. I work @CUTRUSF. Posts are my own.