Maintaining Clean Architecture on Android with Geotriggers using Dependency Injection

2018
0
11-26-2021 12:41 AM
Labels (1)
HudsonMiears
Esri Contributor
0 0 2,018
Maintaining Clean Architecture on Android with Geotriggers using Dependency Injection
 
In this blog I'd like to share an example implementation of Geotrigger monitoring on Android when the app is backgrounded. This example maintains clean architecture and uses dependency injection with Hilt to maintain a separation of concerns between the Geotrigger logic and the implementation of a Foreground Service to monitor location when the app is backgrounded. This blog post has an associated git repo. For an example of monitoring Geotriggers in the background on iOS, check out this associated repo as well.

Defining the problem

Geotrigger monitoring allows us to track a device's location as it enters and exits predefined areas called fences. This functionality is especially useful on mobile platforms such as Android: a smartphone can track its location with a GPS and deliver notifications when it enters a fence, so users can be notified about important environmental information even when they're not looking at their phone.

We've indicated in the guide docs that this can be done using Foreground Services on Android, which let us continuously use location information even when the app is backgrounded. In practice, however, this can be difficult to implement while maintaining a clean app architecture, because Services must be started from Activities and communicated with through binding.

To recap for those unfamiliar: Activities and Services are Android Application Components, meaning that they are the building blocks for your app. An Activity represents a single screen with a user interface, and is usually the first thing you build into an Android app. A Service is used for long-running background processes, and doesn't have a user interface component. A Foreground Service is a Service which is tied to a persistent notification informing the user that work is being done in the background.

The problem lies in keeping the business logic separate from the view logic: the Activity is a view component, but the Service is performing analysis and doesn't need to be displayed. If the Activity is the only way we can communicate with the Service, and our Service needs access to data from the model (the data representation and storage layer of the app), how do we maintain our separation of concerns?

This blog aims to build an example of this pattern using Android architecture components and Hilt dependency injection. The result will be a very simple Geotrigger Monitoring solution which can easily be expanded upon or integrated into a modern Android app.

This app relies on dependency injection provided by Hilt, the library recommended by Android Developers in the Guide to App Architecture. Further information about Hilt on Android is available here. Dependency Injection simplifies our solution to monitoring Geotriggers by allowing us to move Geotrigger business logic into a class which both the Service and ViewModel can observe, but the Activity can ignore.

To use Geotriggers, we also need to access a LocationDataSource. The AndroidLocationDataSource helps us to access Android's location services, but requires an Android Context to create. Context is usually only
accessible from an Activity, but dependency injection provides us with a way to inject a context without worrying how to pass it down through our components.

The basic app architecture

The git repo contains two directories: the GeotriggerMonitoringDemo-WithoutGeotriggers directory contains the app described in this section, and the GeotriggerMonitoringDemo-WithGeotriggers directory contains the same app with our Geotrigger implementation, described below.

To begin with, we've created a basic app to build our Geotrigger Monitoring solution into. Features of the app include:

  • A single Activity containing a Fragment. The Fragment displays a MapView and some buttons.
  • Long-pressing the mapView creates a point of interest at that location, which is saved to a database and displayed as a graphic.
To support these features we've used the following architecture:

  • The database is created with Room, a library for creating and accessing SQLite databases for data persistence. We can access the database using a Database Access Object (DAO).
  • A Repository class exposes the data. In a more complex, online app, this class would interface with a server to ensure data in the database is kept up-to-date.
  • Interactor classes perform business logic on the data. For example, the PointOfInterestInteractor creates the graphics for the points of interest in the database. While this could be done in the ViewModel, we separate these common operations into Interactors so future additional ViewModels could share the logic.
Hilt takes care of dependency injection for us. Note the DatabaseModule class, which provides Hilt with a method to create the database; the @Inject constructors on our Repository and Interactors; and the @HiltViewModel annotation on the ViewModel class. With these simple additions we can avoid the hassle of creating the dependencies for each class in separate factory classes.
 
DemoAppNoGeotriggers.drawio.png
 
The architecture of the app shown above highlights the flow of dependencies. Components higher up on the graph depend on components lower down, but not vice versa. This means that swapping out components will not change the behavior of components above them.

This architecture is largely based on MVVM (Model-View-ViewModel) as outlined in the Guide to App Architecture, with the addition of a "domain layer" to handle some business logic code, based on the implementation from this article.

Foreground Services

While solutions such as WorkManager can be used for long-running background tasks, monitoring location is only permitted on Android when the app is in the foreground. To solve the problem of monitoring location in the background, Android provides us with a Foreground Service.

A Foreground Service is a normal Service which displays a persistent (non-dismissable) notification. To set this up, we only need to create a notification and call startForeground(...). As long as the notification is displayed, the Service will continue to live in memory along with its members, and therefore any operations started from the Service will continue to run.

Adding the Geotrigger Monitoring Service

The code for this section is available at the git repo in the directory GeotriggerMonitoringDemo-WithGeotriggers.

Our solution is built around a GeotriggerInteractor class which creates the Geotrigger, monitor, location data source, and signals to start and stop monitoring. Using Hilt annotations, we mark this class as @Singleton to ensure that only one instance is created. We also add an @Inject constructor taking in the @ApplicationContext. This allows us to create an AndroidLocationDataSource using the application context, which will survive backgrounding. (Note: while this example uses the AndroidLocationDataSource, any LocationDataSource could be implemented this way.)

The signal to start and stop monitoring is implemented as a StateFlow<Boolean> which we can use to access the status from other classes; it can be converted to a LiveData<Boolean>, but can outlive any lifecycle owners required by a LiveData.

In this example, the Geotrigger is created using a GraphicsOverlay from the MapViewModel. This could arguably break the flow of dependencies in the application, but reduces the code for this sample. Another implementation might instead create a GraphicFenceParameters using graphics from the PointOfInterestInteractor, or a FeatureFenceParameters from an online Feature Service.

The GeotriggerMonitoringService is relatively simple. The key line here is:

 

@Inject
lateinit var GeotriggerInteractor: GeotriggerInteractor​

 

This tells Hilt to inject the Interactor after the Service is created. Because our GeotriggerInteractor is marked as a singleton, we know that it's the exact same instance as the one created for the ViewModel. It's important to annotate the Service class with @AndroidEntryPoint so that it can interface with Hilt. Finally, all we need to do to monitor the Services from here is to subscribe to the shouldMonitor signal as such:

 

GeotriggerInteractor.shouldMonitor.asLiveData().observeForever{...}​

 

The Service can be started by opening the app and stopped from the foreground notification. Test it out by creating some points of interest, starting the Service, and closing the app. When you enter or exit the 50 meter buffer of your points of interest, you should receive a Toast showing your notification message! The neat thing
is that with this approach, you'll get the Toast even when your app is backgrounded.
DemoApp.drawio.png

 

Conclusion

 

This is only one example of implementing Geotrigger monitoring in a clean app architecture. Hopefully it can inspire the method you use to integrate Geotriggers into your app. Please share your own ideas with us in the comments below, as well as any questions you might have about this implementation!

 

Useful Links

 

These resources were very useful to me when creating this demo app: