ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Android: Building self-contained, lifecycle aware testable components

In Android applications, many actions are lifecycle driven, and as an application grows in robustness, so does the actions a lifecycle owner (say an Activity or Fragment) has to manage in its lifecycle callback methods, making the code harder to maintain and test -if not implemented correctly-.

In this article, you’ll see 2 approaches to solving the same problem (inspired from the android docs), one using a “common” -traditional- approach, the other using a “better” solution that makes use of the android architecture lifecycle classes to build a lifecycle aware component.

Prerequisites

Some knowledge about the android architecture Lifecycle components would be nice in order to follow along and understand the rest of this article.

Problem

You want to display the user’s location in your application only if they are logged in. For both proposed solutions below, you have the following 3 classes:

  • MainActivity: The screen where the location should be displayed.
  • MainViewModel: Contains the logic that asynchronously checks a user’s status. For the sake of this article, it simply returns a value of true wrapped in LiveData (But in a real-world scenario, this operation may take some time to complete).
  • LocationListener: The class that handles getting and keeping track of the device’s location. It’s the main focus of the 2 approaches below. It includes 2 main methods: start() and stop() which begin and stop keeping track of the device’s location. For the sake of simplicity, these 2 methods only update the value of a variable status.

Solution 1: The “common” way

A common approach to solving this problem would include an implementation of LocationListener that looks like this.

LocationListener's constructor is given a callback, which is an action to be run once the user’s location is available. The start() and stop() methods called as follows.

As you can see, a variable enabled is needed to call the LocationListener's methods. With more that 1 component to manage and multiple actions to handle in the lifecycle callback methods (onStart(), onPause(), etc) , the activity will probably become robust and contain code that is hard to maintain, let alone to test. One way to tackle this is to extract the LocationListener component’s lifecycle dependent logic into its class, which is what the second solution will try to do.

Solution 2: The “better” way

A better way to approach solving the problem, as hinted above, is to build a component that is:

  • Self-contained: All location-related logic is in 1 single place, making it easier to debug if need be, maintain and build upon.
  • Lifecycle aware: The component observes a LifecycleOwner (like an Activity or Fragment) and reacts accordingly to its lifecycle state changes, decoupling it from the UI.
  • Testable: The component can be isolated and unit tested by simply providing it a dummy Lifecycle.

With this approach, the MainActivity looks cleaner.

Nothing crazy is happening in it, it initializes a LocationListener, observes its view model’s checkUserStatus method, and only calls locationListener.enable() if the user is logged in. Let’s see what the LocationListener class now looks like.

Below are the main things to note about the LocationListener class.

  1. The LocationListener is constructed by passing in a Lifecycle and a Callback (an action to be run once the user’s location is available).
  2. An instance ofLocationListener starts observing its lifecycle once it’s created, and stops observing it once it -its lifecycle owner- is destroyed.
  3. LocationListener only updates its status if enabled is set to true. enabled is set to true after the ViewModel’s checkUserStatus() method returns a result, and due to its asynchronous nature there’s no way to tell whether this occurs before or after the activity’s onStart() method, which is why the enable() method adds the following condition lifecycle.currentState.isAtLeast(STARTED).

With LocationListener now being a self-contained lifecycle aware component, it becomes easy to isolate it and unit test it. Since it requires a Lifecycle instance, you can mock a LifecycleOwner and use it for your tests.

Now, you can use the above TestLifecycleOwner's lifecycle to initialize a LocationListener in your test.

So what should you be testing?

  • That LocationListener begins observing its lifecycle when it’s initialized, and stops observing it when its owner is destroyed.
  • That LocationListener connects, does not connect and disconnects appropriately in different scenarios depending on the order of the lifecycle callbacks and the value of enabled.

You can check out all these scenarios in LocationListenerShould.

You can find all the code above in the following repo on Github.

Conclusion

As many actions in Android applications are lifecycle driven, using lifecycle aware components that react to lifecycle status changes to perform actions helps produce better-organized, decoupled and light-weight components, which in turn goes a long way to building a more maintainable and testable codebase.

For more on Java, Kotlin and Android, follow me to get notified when I write new posts, or let’s connect on Github and Twitter!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (5)

Write a response