Kotlin callbackFlow: A lightweight architecture for location-aware Android apps

Database buyer’s remorse

The legacy approach to sharing locations based on the deprecated LocalBroadcastManager
My initial solution —the Service inserts new locations in Room, and our UI can observe changes from Room using Kotlin Flow
  1. Increased APK size due to dependencies for Room
  2. Increased device storage requirements due to database footprint
  3. Possible execution time penalty for writing to/reading from persistent storage
  4. Increased memory requirements for the portions of Room and data loaded into memory

The solution — callbackFlow

A pure Kotlin Flow-based solution is lightweight and allows all app components to be observers
  • The Service is just another observer — Instead of the Service sending the location updates to the Repository, it observes the Repository via Flow just like the other consumers of location information (in this case, to update an ongoing notification). This means that because the Service isn’t the source of the location updates, other components in the app don’t need to wait for it to bind or start to observe locations.
  • Lifecycle of SharedLocationManager is tied to the lifecycle of the observers — We need to be careful how we configure the SharedLocationManager so it survives the creation and destruction of various app components when there is at least one remaining subscriber. And, when no more app components are subscribed, we can stop listening for new locations.
  • The most recent location is returned instead of multiple locations — We change the Flow to return a single Location instead of List<Location>, primarily because we don’t have a persistent store of locations. However, as we’ll see shortly, if the SharedLocationManager was already running when a new component subscribes there is still a way to get access to locations going back to when the SharedLocationManager started for the first subscriber.

SharedLocationManager and callbackFlow 🌟

  • externalScope — It’s a best practice to pass in scopes versus defining it within the class. We’ll see where this comes from in the next section.
  • replay — Here’s that part where observers could potentially be notified of Locations from before when they subscribed (as long as the SharedLocationManager was already started by a different subscriber). We set it to 0, which means that when subscribers subscribe they will only get location updates going forward. But if we set it to 3, subscribers would get the last 3 Locations (if they exist) upon subscription.
  • started — This is key to defining when the Flow is active. Using SharingStarted.WhileSubscribed(), it will start location updates when the first subscriber appears and immediately stop location updates when the last subscriber disappears. This ensures that as long as one app component has a coroutine that’s observing the flow, the location updates will continue. We just need to be careful to scope the observing coroutine to the lifecycle of that app component, which we will see in the following sections.

Hilt implementation for dependency injection 🔪

Activity

In the while-in-use-location example, the user can start and stop location updates from the UI

Service

The Service observes location updates via Flow to update an ongoing notification

Gradle configuration 🔧

Putting it all together

SharedLocationManager uses a callbackFlow to manage location updates which is observed by the Service and Activity

Closing thoughts

  • For simplicity in this example I haven’t changed where the state of the location tracking is stored, which is in the SharedPreferences. However, note that you could switch to using StateFlow within the SharedLocationManager instead.
  • If you remove the button from the UI and have location updates start and stop when the Activity and Service start and stop, the solution is simpler — we could tie Flow entirely to the lifecycle of the Activity and Service and not need to call locationFlow?.cancel().
  • Unlike the Room example, this solution doesn’t require translation of Android system classes to a separate set of project domain model classes— you can have Activities and Services directly observing system classes like android.location.Location from the Flow directly instead of creating your own com.example.android.whileinuselocation.model.Location object and filling it with the same data. I’ll leave you to debate whether this is a good architecture decision 😁. If you do decide to translate to your own domain model classes, you can use the .map operator on the Flow to help with this.

--

--

--

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.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Cloud Firestore Basics in Android

Signing APKs or libraries for release in Android

How To Create an Android App with Dark Mode

Android 11 One Time Permission Changes

Auto-Mirroring Android Devices With scrcpy

Android App with Python | Platforms for Python Android Development

Hosting native android view inside flutter app

A Small Step to the Code, a BIG Step to Android UI — Reusing Layouts

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Sean Barbeau

Sean Barbeau

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.

More from Medium

Kotlin: Extensions to functions

It has never been easier to understand how to write Unit Tests on Android — Part 1

Retained Dagger Component over Configuration Changes

Understanding MockK Kotlin