Android Jetpack architecture – is it a go or a no-go?

In this blog post, I would like to share my learnings from using the new Android Jetpack architecture components.

Kai Widmer
dreipol

--

Visit www.dreipol.ch to learn more about us.

About half a year ago, we at dreipol chose the new Android Jetpack architecture components to build two new Android applications. Our goal was to gain experience and to broaden our options for future decisions on Android app architectures.

Half a year later we consider Android Jetpack as really powerful and flexible and we would not want to miss it in any of our future projects.

In this blog post, I would like to share our learnings from the past six months and give you an idea of how we build our Android applications.

Overview

First of all, I would like to give you an overview of the basic application architecture this article builds on. In reality, each project is different of course but the basics remain the same:

One important thing to mention is that we use single-activity architectures to develop our applications. Besides the fact that Google recommends this approach, we have also made bad experiences in the past when handling the lifecycle of the activities by using a different one for each view. So, single-activity is our way to go.

For the communication between the various components in the user interface (UI) section and between ViewModels and UseCases, we use LiveData. LiveData is an observable data holder class. Its big advantage is that it is naturally managed by the Android lifecycle. We use LiveData along with data binding for the views.

Next, I would like to explain each of the three sections in the application architecture with a focus on the UI.

UI

To display different views and to navigate, we use the components of the new navigation along with fragments and ViewModels. The main idea is to have one NavigationController with a navigation graph that shows the different fragments as pages in the application.
When it comes to more complex views which, for example, contain bottom-navigation, our approach is to use an additional NavController with its own nav graph.

Let me explain this a little bit further:

Navigation

The new navigation components in combination with the SafeArgs-Kotlin-Plugin are very intuitive to use. Let’s assume you have a navigation graph defined as below:

Now you can simply navigate in your fragment like this:

You can also create the action dynamically in the code:

We encountered only one problem. To our knowledge, it has not been covered in the concept yet but will hopefully be fixed in the future:

The animation bug
Let’s assume you have defined the mentioned navigation graph as a nested navigation graph with an action returning to a fragment:

If we execute the action from the nested navigation graph back to the MainFragment and pop the nested graph from the stack, we would expect the exit animation to be played. Instead of this, it is the pop-exit-animation from the toLogin-Action that is being played. As a result, a very weird animation is shown where the current fragment disappears to the right while the new one appears from the right.

So we ended up creating a workaround by overriding the animation in this case:

And in your fragment:

Now you can override the exit animation by navigating like this:

Data binding

The use of data binding eliminates the annoying need of accessing the different view-components in fragments or activities via findViewById().
You simply bind properties to its values directly in the XML, for example, the text of a TextView:

android:text="@{viewmodel.rapport.clientPhone}"

However, do not put logic in this XML binding expressions beside the fact that it is absolutely possible. Put UI logic always in ViewModels, it is more readable and brings the ability to test it with UnitTests.

One thing that can be a little bit annoying is the need to define custom BindingAdapters and you will definitely face it, at least when it comes to bidirectional bindings or custom view components. However, they are easy to define via annotations and you can put them in a separate file. Here an example of an custom Binding-Adapter for binding android:elevation:

ViewModel and LiveData

For the use of ViewModels the following guidelines should help you to use them properly:

  • Use ViewModels to hold data for the ViewBinding and UI-related logic
  • Never pass fragment or activity instances to the ViewModel
  • Never pass Context or View instances to the ViewModel
  • Load data in ViewModel, not in fragment or activity

To create a ViewModel we use the following helper functions:

Create a ViewModel in a fragment or activity:

We use one ViewModel per fragment and additional ViewModels in the scope of the activity lifecycle.
We use the activity scoped ViewModels, for example, to hold the login state of the application. In the activity, we observe the LiveData about the login state. As soon as the state changes to a logged out user we navigate to the login screen.

Communication between fragments and ViewModels

When navigating, showing alerts, or creating intents, your ViewModel decides what will be executed and when. Note though that you don’t want to execute this in the ViewModel, because it is the responsibility of the View to provide this functionality. Therefore you need a way to communicate with your fragment or your activity.

Here the way to go is to use LiveDatas, which you hold in your ViewModel, and observe with your fragment/activity. Additionally, we use an event-class in combination with LiveDatas to determine if the new value has already been handled. Also check out this blog post from AndroidPub: https://android.jlelse.eu/sending-events-from-viewmodel-to-activities-fragments-the-right-way-26bb68502b24

Here is an example of creating a phone intent after the user has clicked a button:

In the ViewModel:

And in the fragment:

By the way, to observe LiveDatas in fragments DON’T do this:

viewModel.phoneCallEvent.observe
(this, Observer { makePhoneCall(it) })

If the fragment does not get destroyed, for example, by a configuration change, this method of observing LiveDatas will leak because the function onCreateView will be called multiple times without removing any existing observers.

Instead of this DO this:

viewModel.phoneCallEvent.observe
(viewLifecycleOwner, Observer { makePhoneCall(it) })

Use cases

This layer brings an additional abstraction between the UI and data section in our example. I found this very useful to provide a clean interface to access data from the ViewModels in the DataLayer which overall improves the testability of the application.

At this point, we start with multithreading by providing results in LiveDatas. For smaller applications, this abstraction layer might be an overkill and can be passed by providing its functionality directly in the ViewModel.

Data

Usually, we use Realm as a database and retrofit for network calls. We are already experienced in using these frameworks and there is no need for us to switch to any others at the moment.

For Realm in combination with the use case architecture, we open and close a context for each call from the use case. Another option is to have a Realm context living with the ViewModel lifecycle which however has a bad impact on the ability to unit test the ViewModels.

Testing

When it comes to testing the use of the architecture component improves the ability to unit test your application a lot. The ViewModel allows you to test UI logic, for example, validations, without the need to create an instrumentation test.

You only have to make sure that LiveDatas run synchronously by defining a corresponding TestRule, and that’s it. Also check out this blog post: https://dev.to/arthlimchiu/how-to-unit-test-livedata-and-viewmodel-5h7f

Let’s have a closer look at this in a small example, where a phone-number-validation is tested:

Dreidroid

Last but not least I would like to present to you our new project named dreidroid.

Throughout the development of the two applications we had to solve identical problems in both applications, so we have come up with the idea to start this new project «dreidroid». Its goal is to provide functionality for our future Android projects. It also provides a few functionalities as described above like the LiveData Event-class, the ViewModel-CreatorFactory, useful BindingAdapters, and other cool stuff. And the best thing is, it is open-source and ready to be checked out on GitHub ; )

Summary

To conclude, the use of the Android Jetpack architecture components brings a lot of benefits to our application development. Especially the handling of the lifecycle with the help of ViewModels is something I have really started to appreciate.

We will definitely use these components again in future projects.

. . .

Thank you for reading this post, feel free to share and clap it :) or follow me and dreipol on Social Media. For your further reading also check out iOS App Architecture with Coordinators and ReSwift.

medium.com/@kai.widmer
twitter.com/dreipol
www.dreipol.ch

--

--