DANA Tech: A Clean Approach to Request Location Updates using ReactiveX + Dagger 2

Harry Timothy
DANA Product & Tech
7 min readMay 11, 2020
DANA Location Update

In recent engineering practice, both Clean Code and Clean Architecture have been widely accepted in the community to produce maintainable codebase. Uncle Bob has successfully brought forward The Age of Clean Coders where any Technology Evangelist is not only preaching about new technology and its implementation, but also its cleanliness. So in this article, I am going to share a clean approach we use in DANA architecture to request location updates from Android Fused Location Provider.

Fused Location Provider API is accessible by calling a method provided by FusedLocationProviderClient (a subclass of GoogleAPI). If you read the docs you can find 3 different guides with scattered code samples:

  1. Get the last known location
  2. Change location settings
  3. Request location updates

When you gather all pieces together, you can find an implementation as shown below:

Source: https://developers.google.com/location-context/fused-location-provider

However, we cannot adopt above implementation as is to our codebase, because it contains a violation to general principle of Clean Architecture where Presentation Layer should not have the responsibility to make any API call. Then, what is the clean way to request location update?

Let’s find out what we did in DANA! :)

1. Introduction to Clean Architecture

Clean Architecture in DANA

The layers are the main core of a Clean Architecture. In our app, we use three layers: Presentation, Domain, and Data. Each layer should be separated and should not need to know about other layers. It should exist in its own world and, at most, share a small interface to communicate.

Presentation Layer consists of:

  • UI & Lifecycle objects, such as Activities, Fragments, Rich Views, Adapters, ViewHolders, etc.
  • Presenters which handles user interaction, rendering views, and follows MVP pattern.
  • Dagger components and modules.

Domain Layer contains our business rules and provides use cases which reflect the features of our app. This layer is also responsible to create an interaction between Data and Presentation Layer. We make this layer independent, so the business logic can be tested without any dependency to external components.

Data Layer contains Repositories which Domain Layer can use. This layer exposes a data source and emit the stream down to Presentation Layer.

Some of Clean Architecture sample code we can find today often separates each layer into different packages within a single (:app) module. In DANA project structure, however, we separate each layer into different modules. We also use Package-by-feature to place all items related to a single feature into a single directory/package. This results in packages with high cohesion and high modularity, with minimal coupling between packages.

Classes in each layer follows this Naming Convention:

File Naming Convention in DANA

As shown in above diagram, each file has a very strong-formatted naming, thus can create a common terminology across engineers which can help to easier the process of collaboration.

2. Requesting Location Updates with Clean Architecture

To further explain how we request location updates in clean way, we will focus on the Data Layer first. Assuming our codebase has a Data Layer architecture as shown in the diagram below:

Data Layer Architecture

As we know, it is possible to have more than one data source in our codebase. The data retrieved from any data source is called Entity Data. It is basically an interface to access different data from different sources, as shown below:

We name the interface LocationEntityData because this entity data comes from Location Services, which is a part of Location feature. Any entity data which from any data source should implement getLocation() function. This function acts as a facilitator for LocationRepository to get location data.

The next step is to create classes for each data source which implement above interface. If the data comes from network (e.g. HTTP requests), we should name the class NetworkLocationEntityData, and implement LocationEntityData interface. If it comes from local (e.g. persistent storage), we should name it LocalLocationEntityData. As for the Mock classes, we name it MockLocationEntityData to enforce readability and clear separation by only looking to its name.

We can assume location data comes from network source because we need Location Services to request location updates. Also, the data is actually coming from either WiFi, cell tower positioning, or GPS. So, we can put the whole implementation inside NetworkLocationEntityData class:

Dagger 2 is used to inject Application Context which is required to create Fused Location Client instance. It is also used to mark above class’ instance as Singleton, so we can reuse the same instance within Application lifetime. Then we make getLocation() returns locationSubject, and emit values to this subject after we get the result in locationCallback.

As for the ReactiveX part, actually we can consider using BehaviorSubject instead of PublishSubject in case we want to emit one last cached emission before subscription. However, for demonstration purpose we only need to emit new location data to the subscribers without replaying the last emitted element. Therefore we are using PublishSubject.

The next thing to do is to create this Entity Data in Data Factory:

Data Factory only have one responsibility: to return the instance of Entity Data according to its source. According to the naming convention found in part 1, we should name this class LocationDataFactory so we can quickly identify its responsibility: to create LocationEntityData. This implementation also requires us to inject Application Context as a mandatory parameter to create NetworkLocationEntityData. Source can be either constants, enums, or anything to differentiate the data source.

The next step is to create Entity Repository. But to do this, we need to firstly create an interface which consists of the required methods at Domain Layer, so we can access it later from our Use Case class. This interface is called Repository.

Considering the naming convention, we name this class LocationRepository as it only has one responsibility: to act as a bridge between Domain and Data Layer, so the Use Case can access the created Location data. When browsing code samples of Clean Architecture in GitHub, some people in the community doesn’t introduce Repository as an interface, but rather an implementation class. Although it is debatable whether such thing should be acceptable or not, in DANA we choose to code to interfaces, not to implementation, as a part of our OO practice. Therefore, we define Repository as an interface.

After creating LocationRepository, we go back to Data Layer and create implementation class of this interface, and inject LocationDataFactory inside.

LocationEntityRepository is responsible to implement every methods we have in LocationRepository. We implement getLocation() function in this class, which retrieves the data from Network source via createNetworkData() function. If an error happens during data creation, the data will be retrieved from LocalLocationEntityData instead using onErrorResumeNext(). Finally, we cache the location data with saveLocationData().

The next step is to call this Repository from Domain Layer, inside a use case, before we can finally consume the Observable in Presentation Layer.

There are some new components introduced this class:

  1. ThreadExecutor: An interface to execute use cases in background thread.
  2. PostExecutionThread: An interface which defines the threading method.
  3. UseCase: A base class for every use case class.

Since all of these components are parts of Clean Architecture Boilerplate, we can just copy-paste and use it as is. Our only effort is to implement buildUseCaseObservable() method, and call getLocation() method from locationRepository. We set the param type as Unit (or Void in Java) because getLocation() does not need any parameter. As for the return type, we set it as String because the result is defined as String in Data Layer.

After creating the use case, finally we can move to Presentation Layer. Because our Presentation Layer is using MVP Architectural Pattern, the first thing to do is to create the Contract.

The main purpose of this contract is to establish a coupling which facilitates one-to-one communication between a presenter and a view. Presenter interface has two methods: getLocationUpdates() and onDestroy(), while View interface also has two methods: both to handle the use case result whether it is success or failed.

Now we can create the Presenter class!

LocationPresenter only has one responsibility: to handle the view logic when location result is retrieved from the use case. To do this, we firstly implement LocationContract.Presenter methods:

  • getLocationUpdates(): Triggers the use case execution.
  • onDestroy(): DisposeCompositeDisposable in all use cases.

We use DisposableObserver to handle the data which comes from getLocationUpdates use case. If location request is success, the data will be retrieved from onNext callback, then onComplete callback will be called afterwards. But if there is an error or thrown exception during the process, the app will enter onError callback instead.

If you are wondering how to implement the Presenter to an Activity, I have created a demo app: SimpleLocationApp, and I encourage you to check the source code as you can find some good examples of implementing the MVP part, as well as the DI part.

SimpleLocationApp

So that’s it for now. Hopefully I can share more stories regarding Android Apps Development at DANA in the near future.

Thank you very much for reading!

--

--

Harry Timothy
DANA Product & Tech

A native Android Developer who stop being a programmer once, but made an epic comeback thanks to Kotlin and Uncle Bob.