Clean architecture on Android

Auteur
Nick Martens
Datum

I went to AppDevCon in Amsterdam, and among others I saw two very interesting talks about mobile app architecture. First, "Getting Clean, Keeping Lean" by Joe Birch (Buffer) and second, "Hidden mysteries behind big mobile codebases", by Fernando Cejas (Soundcloud). Both of them discussed the architecture applied to their apps, and they got me thinking about app architecture.. again. In this post I will share some of my findings with you.

Clean architecture

Both Joe and Fernando actually refer to something called clean architecture. Clean architecture proposes four layers, to decouple application components, improve code quality and simplify maintenance. Strictly decoupling the layers is probably the most important thing to keep in mind when using clean architecture. Let's take a closer look at those layers.

The first, outermost layer is "Frameworks and Drivers" containing everything platform specific. In case of Android this contains things like custom views, fragments, activities and the native storage methods like SharedPreferences and Sqlite.

The second layer (or module) contains presenters aka. "interface adapters". This layer build on the underlying layer and adapts it to the UI (frameworks and drivers) layer. The UI delegates almost everything to this layer to decouple it from the native framework as much as possible.

The third layer contains "business rules", or "use-cases". It has for example methods to retrieve data, for a specific use-case, from the underlying layer. Albeit from the server or from local cache. Where it comes from does not matter. This layer only knows about the layer it is wrapping, the domain logic or entity layer.

Entity layer

The "entity (or domain logic) layer" contains the actual methods to fetch the data from wherever it needs to come from and provides it's own representation of this data. It also does not know anything about native networking, communication or shared preferences. These are implementation details hidden behind interfaces. Visually these layers look like an union, each layer wrapping the other. These layers look like this:

clean-architecture-android 1

As you can see each layer is wrapped by the next. You can also see the dependencies are pointing inwards. So presenters is only aware of use cases, not entities, and UI is only aware of presenters, not use cases or entities. Note that if a layer does not work for you, because for some reason it does not make sense, don't hesitate to skip it. Or in case you need an additional layer because that makes total sense four your app, just add it.

Testability Both speakers, Joe and Fernando, explained how this architecture led to better testability of their applications. Which brings me to the next point: "Because only the Frameworks and Drivers layer, is coupled to the Android framework, all other code can (and should) be plain java modules." Joe even takes it a step further and argues that by first having the inner three layers in place, you can work on stability even before you start working on the UI.

Plain Java Modules

I am talking about modules in the application in this case. In Android's case this means a separate Gradle module for each layer. Because this allows you to develop them as normal java code, with normal unit tests. It also forces dependency inversion on you because you can't access components upwards. And because it is plain java, you do not require a phone or an emulator to run your tests. Which means, faster build times and a faster development cycle. One thing to keep in mind is that the models from one layer should not be reused in another layer. This may seem like overhead, but simple delegates can minimize the impact of this. Joe proposes to use mapper object that convert the model object to your own type as soon as they cross the layer's boundaries. You could choose to utilize Android's annotations for this as well. There are annotations to mark classes and packages as part of a certain group and you can then restrict access from other groups. For example @RestrictTo(RestrictTo.Scope.GROUP_ID).

clean-architecture-android 2

Mixing with databinding I like Android's databinding framework a lot, so I have been thinking about the best way to mix clean architecture with Android's databinding framework. It is actually very simple. You need just two components for each of your bindings. The first component is a data model and the second component is a behavioral delegate. The behavioral delegate receives all ui-events and forwards them to the presenter. And in turn the presenter can act as needed. The data model is simply the object containing your data. You could also use two-way binding to make it easier to send updates events straight into the presenter-layer. This would result in activities and fragments that almost don't contain any code, which obviously leads to a better separation of concerns. And because the next layer contains most of the complexity, and because it is a plain java module, it is a lot easier to test.

LightCycle Which brings me to a library Fernando showed. At Soundcloud they developed a library called LightCycle, which allows you define objects within your activities and fragments, that automatically get the required lifecycle events forwarded to them. This allows you to create separate controllers for your activities and fragments. Because these controllers are no longer hard-wired to the activity or fragment, you can instantiate them in your tests and easily simulate the activity's or fragment's behavior.

Mixing with Reactive frameworks

When you combine clean architecture with a reactive framework (which seem to be used by almost everybody presenting at the conference) this architecture really starts to shine. It makes it very easy to automatically refresh the ui whenever an update happens. The architectural diagram of this look as follows:

clean-architecture-android 3

As you can see the data is backed by observable repositories, this can for example be an RX-observable or an Agera repository. So registering a listener in the top (ui) layer, will cause the observable to be observed and load its data. It would be very simple to map one type into the other using Agera. Let's take a look at the following code sample, we could place at a layer boundary:

// Source layer, for example injected using DI
Repository<Result<SourceType>> mSourceRepo;
// My layer, mapped from source value 
// could be injected as a singleton
MutableRepository<Result<LayerType>> mDataRepo;
// On update of layer below
void doUpdate() {
  mSourceRepo.get()
   .ifSucceededAttemptMap(DataMapper::map)
   .ifSucceededSendTo(mDataRepo);
}

As you can see we can observe the source repository. In the doUpdate method, which handles the update, we can map the value to the correct type for this layer. Within this layer the local data repository is exposed and can be observed by the next layer. Remember that observing repositories should be driven by the lifecycle of the components to prevent leaks. Usually this would mean subscribing from the onStart callback en unsubscribing from the onStop callback.

Package structure Before concluding I would like to take a little side-step. One of the things that struck me most in Joe Birch's talk is that he argues that by seeing the package structure of an application you should be able to see what the application does. He argues code organization starts with a clear and understandable package structure. Having a package named activities, fragments, views and adapters tells nothing about the application (besides that is probably is an Android application), it only tells you where to find what kind of classes and not what they actually do. So, imagine you are new, and you need to change something in the instant messaging component of the app, wouldn't it make sense if that was somewhere in a package named messaging or im?

Conclusion

Obviously every architecture or way of working has its downsides. Joe also points out the following disadvantages to clean architecture:

  • Adds initial overhead
  • Takes time to get used to
  • Can feel like overkill for some tasks
  • Difficult to make decisions

However, the advantages easily outweigh the disadvantages:

  • High test coverage
  • Easy to navigate package structure
  • Easier to maintain
  • Allows us to move faster
  • Focussed classes and test classes
  • Separation of concerns
  • Define, test, stabilize before UI
  • Futureproof implementations

I hope you agree that these advantages by far outweigh the disadvantages. To summarize, using this architecture should allows us to develop faster, test better and apply solid principles by design. Using the correct tools allows you to move even faster because you need less boiler plate. I must say I am really looking forward to experiencing this architecture. Perhaps in the next project I'll be working on there will be an opportunity to try this.

One final thing, by special request

For those stuck on the iOS platform, as you can see, the only layer that is really bound to the platform is the same layer that is allowed to use platform specific APIs. This is the UI layer. This would mean that instead of communicating with the presenter layer from an activity or fragment, you could do so from your view-controller. I tried searching for images explaining this architecture on iOS, but I keep on finding images similar to the images I already used. So I guess this means these principles can be applied as easily to iOS as to Android.

[Images used in this article come from https://github.com/android10/Android-CleanArchitecture)]

Tags

Mobile Android Development