Android Test-Driven Development by Tutorials,
Second Edition – Now Updated!

Build testable, sustainable Android apps via JUnit, Mockito, and Espresso
by diving into test-driven development (TDD) in this newly-updated book.

Home Android & Kotlin Tutorials

Lifecycle-Aware Components Using Android Jetpack

Learn about lifecycle-aware components including what they are, how they work, how to implement your own components and how to test them.

5/5 3 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

Android Jetpack is a collection of libraries that help developers improve their code, reduce boilerplate code and make their app work consistently across different Android versions. Jetpack’s Android Architecture Components provide tools to help implement lifecycle-aware components that react to lifecycle changes in activities or fragments.

In this tutorial, you’ll create a lifecycle-aware component in an app named AwarenessFood. This component will improve how the app handles network connection changes. You’ll also create a lifecycle owner that will communicate the network state to the activity.

The app shows a random recipe to the user and has two menu options: one to get a new random recipe and the other to show some food-related trivia. When the device is offline, the main screen shows a snackbar with a message and a retry option.

Over the course of this tutorial, you’ll learn about:

  • Lifecycles in Android
  • Lifecycle-aware components
  • Lifecycle observers
  • Events and states
  • Lifecycle owners
  • How to test your lifecycle-aware components
  • LiveData
Note: This tutorial assumes you know the basics of Android development. If you’re new to Android development, check out this Android Tutorial for Beginners.

Getting Started

Download the materials using the Download Materials button at the top or bottom of this tutorial. Open Android Studio 4.2.1 or later and import the starter project.

Below is a summary of what each package does:

  • analytics: Contains classes for tracking app events.
  • data: Contains model classes.
  • di: You’ll find classes for providing dependency injection here.
  • monitor: Contains a single class for observing network connectivity.
  • network: Here, you’ll find classes for accessing external APIs.
  • repositories: Has classes for managing persistence.
  • viewmodels: Contains business logic classes.
  • views: Holds a custom View.

Registering for the spoonacular API

AwarenessFood uses the spoonacular API to fetch recipes. You’ll need to register for this API to be able to run the app successfully.

Go to the spoonacular website and create a new account. Once you confirm your account, log in and go to your profile to find your API key. Copy it, open RecipesModule.kt inside the di package and replace the value in the following line:

private const val API_KEY = "YOUR_API_KEY_HERE"

Build and run. You’ll see a screen with a random recipe, similar to the one shown below. Bonus points if you get the same recipe as in the image. :]

AwarenessFood app running

To get another random recipe, press the Reload button in the action bar. If you try to get a new recipe and your device goes offline, you’ll see a snackbar with the error message and retry button, as shown below:

Network error showing in a snackbar.

To go to the food trivia screen, press the Food Trivia option in the More menu. You’ll implement this functionality later in the tutorial. Right now, you’ll only see a button to get the food trivia, as shown below:

Food trivia section in the app

This completes the setup you need to run the app. Now, you’re ready to learn about lifecycle-aware components.

Lifecycles in Android

An important basic concept you need to understand as an Android developer is how the lifecycle of activities and fragments work. The lifecycle is a series of callbacks executed in a certain order when the status of the activity or fragment changes.

The lifecycle is important because certain actions need to take place when the activity or fragment is in a specific state. For example, setting the activity layout needs to take place in its onCreate().

In a fragment, you need to create the view and set its layout in onCreateView(). Another example is enabling the current location reading in onStart().

For the destruction process, the location reading should stop in onStop(), which is also where you need to unregister other components. It’s important to know that not all the callbacks get executed every time. For example, the operating system may or may not execute onDestroy().

The following diagram shows the complete lifecycle for activities:

Activity Lifecycle

If you want to know more about activity’s lifecycle, go to our Introduction to Android Activities With Kotlin tutorial.

This diagram shows the lifecycle for fragments:

Fragment Lifecycle

If you want to know more about a fragment’s lifecycle, read Android Fragments Tutorial: An Introduction With Kotlin.

Reacting to Lifecycle Changes

Most apps have multiple components that need to react to the lifecycle of an activity or fragment. You need to initialize or register these components in onStart() and unregister or perform some cleanup in onStop(). In some cases, you need to do some other actions during another lifecycle callback.

Following this pattern, your code can become messy and error-prone. The code within onStart() and onStop() will expand indefinitely. Meanwhile, it’s easy to forget to unregister some of your components or to call the component’s methods in the wrong lifecycle callback, causing bugs, memory leaks and crashes.

You can see some of these problems in the app right now. Open NetworkMonitor.kt in the starter project. This class is in charge of listening to the state of the network connection and notifying the activity if the state of the connection changes.

Note: For more information about monitoring the network connection, visit the Read Network State documentation.

The NetworkMonitor instance needs to be available from the initialization of the activity. Open MainActivity.kt, which contains the code to initialize it in the onCreate() by calling networkMonitor.init().

NetworkMonitor then registers the network callbacks in onStart() by calling networkMonitor.registerNetworkCallback(). Finally, it unregisters these callbacks in onStop() by calling networkMonitor.unregisterNetworkCallback().

Initializing the component, registering the callbacks and unregistering them again add a lot of boilerplate code to the activity. Besides, you only need to add onStart() and onStop() to call the NetworkMonitor methods.

In MainActivity, there’s only one component that needs to react to lifecycle changes. However, in a bigger and more complex app, several components need to do the same, it can become a complete mess.

Using Lifecycle-Aware Components

NetworkMonitor performs different actions that depend on the state of the lifecycle of the activity where it lives. In other words, NetworkMonitor needs to be a lifecycle-aware component and react to changes in the lifecycle of its parent — in this case, MainActivity.

Jetpack provides classes and interfaces to help create lifecycle-aware components. By using them, you can improve the way NetworkMonitor performs its actions. These actions will execute automatically according to the current lifecycle state of its parent activity.

A lifecycle owner is a component that has a lifecycle, like an activity or a fragment. The lifecycle owner needs to know all the components that need to listen for its lifecycle changes. Using the observer pattern is the best approach to achieve this.

Creating a Lifecycle Observer

A lifecycle observer is a component that has the ability to observe and react to the lifecycle state of its parent. Jetpack provides the LifecycleObserver interface to transform a class into a lifecycle observer.

It’s time to start upgrading NetworkMonitor. Open NetworkMonitor.kt and make it implement LifecycleObserver, like so:

class NetworkMonitor @Inject constructor(private val context: Context) : LifecycleObserver {
 // Code to observe changes in the network connection.
}

That’s it. Now, NetworkMonitor is a lifecycle observer component. You’ve completed the first step to convert it to a lifecycle-aware component.

Lifecycle Events and States

The Lifecycle class is responsible for knowing the lifecycle state of the parent and communicating it to any LifecycleObserver that’s listening.

Lifecycle uses two enums to manage and communicate the lifecycle state: Event and State.

Events

Event‘s values represent the lifecycle events that the operating system dispatches. The available values for Event are:

  • ON_CREATE
  • ON_START
  • ON_RESUME
  • ON_PAUSE
  • ON_STOP
  • ON_DESTROY
  • ON_ANY

Each value is equivalent to the lifecycle callback with the same name. ON_ANY is different because all lifecycle callbacks trigger it.

Reacting to Lifecycle Events

NetworkMonitor is now a LifecycleObserver, but it doesn’t react to any lifecycle changes yet. To make it do so, you need to add the @OnLifecycleEvent annotation to the method that will react to a specific lifecycle change. You use a parameter to indicate which lifecycle event it will react to.

Add @OnLifecycleEvent to the different methods, as follows:

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun init() {
// ...
}

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun registerNetworkCallback() {
// ...
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun unregisterNetworkCallback() {
// ...
}

In this case, NetworkMonitor needs its init() to react to the ON_CREATE event. registerNetworkCallback() reacts to the ON_START event and unregisterNetworkCallback() to ON_STOP.

Now that NetworkMonitor can react to lifecycle changes, you need to do some cleanup. You no longer need the following code in MainActivity.kt because NetworkMonitor now performs these actions itself.

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  // 1.Network Monitor initialization.
  networkMonitor.init()
  // ...
}

// 2. Register network callback.
override fun onStart() {
  super.onStart()
  networkMonitor.registerNetworkCallback()
}

// 3. Unregister network callback.
override fun onStop() {
  super.onStop()
  networkMonitor.unregisterNetworkCallback()
}

Remove onStart() and onStop() completely. Also remove the networkMonitor.init() line from onCreate().

By making these changes, you’ve moved the responsibility to initiate, register and unregister the component from the activity to the component itself.

States

State holds the current state of the lifecycle owner. The different available values are:

  • INITIALIZED
  • CREATED
  • STARTED
  • RESUMED
  • DESTROYED

These states are useful when an action in a lifecycle-aware component needs to know if a specific event has occurred.

One example is when a long operation runs during the ON_START event and the activity or fragment is destroyed before that operation completes. In this case, the component shouldn’t execute any action during the ON_STOP event, since it never completely initialized.

There’s a relationship between the events and the states of the lifecycle. The following diagram shows this relationship:

Lifecycle States and Events

Here’s when those states occur:

  • INITIALIZED: When the activity or fragment is already constructed but before onCreate() gets executed. This is the initial state of the lifecycle.
  • CREATED: After ON_CREATE and after ON_STOP.
  • STARTED: After ON_START and after ON_PAUSE.
  • RESUMED: Only occurs after the ON_RESUME.
  • DESTROYED: After ON_DESTROY but right before the call to onDestroy(). Once the state is DESTROYED, the activity or fragment won’t dispatch any more events.

Using Lifecycle States

Sometimes, components need to execute some code when its parent is at least in a certain lifecycle state. You need to ensure that NetworkMonitor executes registerNetworkCallback() at the correct moment.

Add a Lifecycle parameter in the constructor of NetworkMonitor, as follows:

private val lifecycle: Lifecycle

With this, NetworkMonitor has access to its parent lifecycle state through the lifecycle variable.

Now, wrap all the code in registerNetworkCallback() with the following condition:

fun registerNetworkCallback() {
  if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
    // ...
  }
}

With this condition, NetworkMonitor will only start monitoring the network connection when the parent lifecycle is at least in the STARTED state.

This comes in handy because the parent’s lifecycle can change before the code execution completes in the component. Knowing the state of the parent can help avoid crashes, memory leaks and race conditions in the component.

Finally, open MainActivity.kt and modify the creation of NetworkMonitor as follows:

networkMonitor = NetworkMonitor(this, lifecycle)

NetworkMonitor now has access to its parent’s lifecycle and will only start listening to network changes when the activity is in the right state.

Subscribing to Lifecycle Events

For NetworkMonitor to actually start reacting to lifecycle changes, its parent needs to know about its existence so it can dispatch its lifecycle events to the component.

In MainActivity.kt, add the following line in onCreate() after the line where you initialized networkMonitor:

 lifecycle.addObserver(networkMonitor)

This tells the lifecycle owner that NetworkMonitor will listen to its lifecycle events. If there are multiple components that need to listen to these changes, you can add them to the lifecycle owner as observers without a problem.

This is a great improvement. With only a single line, the component will now receive the lifecycle changes from its lifecycle owner. You don’t need boilerplate code in the activity anymore. Besides, the component itself holds all the initialization and configuration code, making it self-contained and testable.

Build and run the app again. After the recipe loads, put the device in airplane mode. You’ll see the network connection error snackbar appear, as it did before.

Making the snackbar lifecycle-aware. Network error showing in a snackbar.

Behind the scenes, however, you’ve vastly improved the implementation.

Who Owns the Lifecycle?

Everything looks great, but… who owns the lifecycle? Why does the activity own the lifecycle in this example? Are there any more owners?

A lifecycle owner is a component that implements the LifecycleOwner interface. This interface has one method that the owner needs to implement: Lifecycle.getLifecycle(). Basically, any class that implements this interface can be a lifecycle owner.

Android provides built-in components that are lifecycle owners. For activities, ComponentActivity, which is the base class for AppCompatActivity, is the one that implements LifecycleOwner.

However, there are other classes that already implement this interface, too. For example, Fragment is a LifecycleOwner. This means that you can move this code to a Fragment, if needed, and it will work the same way as in MainActivity.

The lifecycle of fragments can be considerably longer than the lifecycle of the view they contain. If an observer interacts with the user interface in a fragment, this can cause a problem because the observer can modify a view before it’s initialized yet or after it’s destroyed.

That’s why you can find a viewLifecycleOwner within Fragment. You can start using this lifecycle owner during onCreateView() and before onDestroyView(). Once the view lifecycle gets destroyed, it won’t dispatch any more events.

A common use of viewLifecycleOwner is to observe LiveData in a fragment. Open FoodTriviaFragment.kt and, in onViewCreated(), add the following code right before viewModel.getRandomFoodTrivia():

viewModel.foodTriviaState.observe(viewLifecycleOwner, Observer { 
  handleFoodTriviaApiState(it)
})

You need to add this import too: import androidx.lifecycle.Observer.

With this code, FoodTriviaFragment will react to foodTriviaState, which is a LiveData. Since the observer has viewLifecycleOwner as its owner, it will only receive events while the fragment is in an active state.

It is time to build and run the app. Tap the More menu option and select Food Trivia. Now you are able to get some fun and interesting Food Trivia in your app.

Using ProcessLifecycleOwner

In some cases, a component needs to react to the application lifecycle changes. For example, it might need to track when the user sends the app to the background and when it comes back to the foreground. For those cases, Jetpack provides ProcessLifecycleOwner, which implements the same LifecycleOwner interface.

This class provides a lifecycle for the entire app process. It dispatches ON_CREATE only once, when the app starts for the first time. It won’t dispatch ON_DESTROY at all.

ProcessLifecycleOwner dispatches ON_START and ON_RESUME when the first activity in the app goes through these states. Finally, ProcessLifecycleOwner dispatches ON_PAUSE and ON_STOP events after the last visible activity of the app goes through them.

It’s important to know that these last two events will happen after a certain delay. There’s a delay because ProcessLifecycleOwner needs to be sure of the reason for these changes. It only needs to dispatch these events if they happen because the app went to the background and not due to configuration changes.

When using this lifecycle owner, a component needs to implement LifecycleObserver, as usual. Open AppGlobalEvents.kt in the analytics package. You can see that it’s a LifecycleObserver.

Its functionality is to track whenever the app comes to the foreground or goes to the background. As you can see in the code, this happens when the lifecycle owner sends the ON_START and ON_STOP events.

You need to register this LifecycleObserver in a different way. Open RecipesApplication.kt and add the following code in onCreate():

  ProcessLifecycleOwner.get().lifecycle.addObserver(appGlobalEvents)

With this code, you get an instance of ProcessLifecycleOwner and add appGlobalEvents as its observer.

Now, build and run the app. After the app starts, send it to the background. Open Logcat, filter by the tag APP_LOGGER and you’ll see the following messages:

AwarenessFood Logcat messages

So far, you’ve seen how fragments, activities and application components implement the LifecycleOwner interface. But that’s not all. You’ll now see how to create custom classes that do the same thing.

Creating a Custom Lifecycle Owner

Remember that any class can implement the LifecycleOwner interface. This means that you can create your own lifecycle owner.

You’ll create a custom lifecycle owner that starts its lifecycle when the device loses the network connection and ends its lifecycle whenever it recovers connectivity.

Open UnavailableConnectionLifecycleOwner.kt in the monitor package and modify the class to implement LifecycleOwner:

@Singleton
class UnavailableConnectionLifecycleOwner @Inject constructor() : LifecycleOwner {
// ...
}

Next, add the following LifecycleRegistry to UnavailableConnectionLifecycleOwner as follows:

private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)

override fun getLifecycle() = lifecycleRegistry

This LifecycleRegistry is an implementation of Lifecycle that can handle multiple observers and notify them of any changes in the lifecycle.

Adding Events

You can use handleLifecycleEvent() to notify whenever lifecycle events occur. Add the following methods to the class:

fun onConnectionLost() {
  lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

fun onConnectionAvailable() {
  lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}

In this case, whenever the device loses the connection, lifecycleRegistry will send the ON_START event to its observers. It will send ON_STOP whenever the connection becomes available.

Finally, add the following code:

fun addObserver(lifecycleObserver: LifecycleObserver) {
  lifecycleRegistry.addObserver(lifecycleObserver)
}

addObserver() will register any observer that wants to receive UnavailableConnectionLifecycleOwner lifecycle events.

Reacting to Events

Now, open MainActivity.kt and add the following code in onCreate():

unavailableConnectionLifecycleOwner.addObserver(networkObserver)

This will make networkObserver react to unavailableConnectionLifecycleOwner events. NetworkObserver will show the snackbar whenever the device loses the connection and hide it whenever the connection is back.

Finally, replace handleNetworkState() with:

private fun handleNetworkState(networkState: NetworkState?) {
  when (networkState) {
    NetworkState.Unavailable -> unavailableConnectionLifecycleOwner.onConnectionLost()
    NetworkState.Available -> unavailableConnectionLifecycleOwner.onConnectionAvailable()
  }
}

This code will trigger unavailableConnectionLifecycleOwner‘s lifecycle events.

Finally, build and run the app. It works as before, except that the app now uses a custom lifecycleOwner to handle network events.

How do you write tests for lifecycle-aware components? You’ll learn that in the next section.

Testing a Lifecycle-Aware Component

Another benefit of having all lifecycle-aware code in NetworkMonitor is that you can test the code according to the lifecycle event that it needs to react to.

These tests will verify that the correct method within NetworkMonitor executes according to the lifecycle owner’s state. Building the test will be similar to the steps you followed when implementing a custom lifecycle owner.

Setting Up The Tests

Open NetworkMonitorTest.kt. To start, the test needs to mock both a lifecycle owner and the network monitor. Add the following two mocks right after the class’s opening braces:

private val lifecycleOwner = mockk<LifecycleOwner>(relaxed = true)
private val networkMonitor = mockk<NetworkMonitor>(relaxed = true)

In the code above, mockk is a function provided by the MockK library that helps you create mocked implementations of a certain class. The relaxed attribute specifies that you can create mocks without specifying their behavior.

Next, add the following variable after the variables you just added:

private lateinit var lifecycle: LifecycleRegistry

This adds LifecycleRegistry to the test, which will handle the observers and notify them of changes in its lifecycle.

Finally, add the following to setup():

lifecycle = LifecycleRegistry(lifecycleOwner)
lifecycle.addObserver(networkMonitor)

This initializes the lifecycle registry, passing the lifecycle owner mock, and adds the NetworkMonitor so it can observe the lifecycle changes.

Adding The Tests

To verify that the correct method executes in NetworkMonitor, the lifecycle registry should set the correct lifecycle state and should notify its observers. You’ll use handleLifecycleEvent() to achieve this.

The first test you’ll write verifies that the ON_CREATE event triggers init().

Implement this using the code below:

@Test
fun `When dispatching On Create lifecycle event, call init()`() {
  // 1. Notify observers and set the lifecycle state.
  lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)

  // 2. Verify the execution of the correct method.
  verify { networkMonitor.init() }
}

In the code above, you:

  1. First, set and notify the lifecycle state.
  2. Then, verify that init() executed in NetworkMonitor.

Finally, add the following imports:

import androidx.lifecycle.Lifecycle
import io.mockk.verify
import org.junit.Test

Execute the test — and it’s successful.

Add the following code to test that the START lifecycle event calls registerNetworkCallback():

@Test
fun `When dispatching On Start lifecycle event, call registerNetworkCallback()`() {
  lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)

  verify { networkMonitor.registerNetworkCallback() }
}

This verifies that ON_START triggers registerNetworkCallback().

Execute the test and it will pass.

Finally, create a test to verify unregisterNetworkCallback(). In this case, the test needs to dispatch ON_STOP.

Use the following code to achieve this:

@Test
fun `When dispatching On Stop lifecycle event, call unregisterNetworkCallback()`() {
  // 1. Notify observers and set the lifecycle state.
  lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

  // 2. Verify the execution of the correct method.
  verify { networkMonitor.unregisterNetworkCallback() }
}

This verifies that ON_STOP triggers unregisterNetworkCallback().

Run the test and.. it fails with this error: Verification failed: call 1 of 1: NetworkMonitor(#2).unregisterNetworkCallback()) was not called.

Does this mean that unregisterNetworkCallback() didn’t execute with the ON_STOP event? The test is trying to dispatch the ON_STOP event, but according to the lifecycle flow, at least ON_START should occur before ON_STOP.

Now, try this approach. Add the code to dispatch ON_START first:

lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

Run the test — now, it’s successful.

As you can see, it’s really easy to test if the correct lifecycle events trigger the correct methods in NetworkMonitor.

LiveData: A Lifecycle-Aware Component

So far in this tutorial, you’ve seen how to create your own lifecycle-aware components. But are there any lifecycle-aware components already available in Android? The answer is yes, and maybe the most famous of them is LiveData.

The concept behind LiveData is simple. It’s an observable data holder class, which means it can contain data and it will notify its observers when that data changes. However, the nice thing about LiveData is that it’s lifecycle-aware, meaning that it only updates its observers when the lifecycle is in an active state.

An observer is in active state if its lifecycle is in the STARTED or RESUMED state. If the lifecycle is in another state, then LiveData won’t notify its observers of any changes.

Creating and Assigning LiveData Variables

Open MainViewModel.kt from the viewmodels package. Inside, add the following variables:

private val _loadingState = MutableLiveData<UiLoadingState>()
val loadingState: LiveData<UiLoadingState>
  get() {
    return _loadingState
  }

loadingState is a LiveData that can have two possible values: Loading and NotLoading. This value will tell the view if it needs to display or hide the progress bar.

Whenever a LiveData variable gets a new value, it notifies its observers about the change. To achieve this, you use its value property.

Modify getRandomRecipe() as follows:

fun getRandomRecipe() {
  _loadingState.value = UiLoadingState.Loading
  viewModelScope.launch {
    recipeRepository.getRandomRecipe().collect { result ->
      _loadingState.value = UiLoadingState.NotLoading
      _recipeState.value = result
    }
  }
}

Now, updating value will notify all the observers of the updates to _loadingState.

Observing LiveData Changes

Open MainActivity.kt and add the following in its onCreate():

viewModel.loadingState.observe(this, Observer { uiLoadingState ->
  handleLoadingState(uiLoadingState)
})

With this code, MainActivity will start observing updates on viewModel.loadingState. As you can see, the first parameter in observe() is this, which is the current instance of MainActivity. Look at the signature of observe() and you’ll see that its first parameter is a LifecycleOwner. This means that LiveData‘s observers will react depending on the state of the activity’s lifecycle. Control– or Command-click observe to inspect the method signature.

Look within observe():

if (owner.getLifecycle().getCurrentState() == DESTROYED) {
  // ignore
  return;
}

Here, you see that if LifecycleOwner is DESTROYED and a new value is set to the LiveData variable, then the observer will do nothing.

However, if the LifecycleOwner becomes active again, the observer will receive the last available value automatically.

Build and run. The app will show a progress bar while loading the data. It will hide the progress bar after the data loads.

Congratulations! You’ve successfully migrated an app to use lifecycle-aware components.

Where to Go From Here?

You can download the final project by using the Download Materials button at the top or bottom of the tutorial.

Jetpack offers other libraries of Android architecture components. To get to know more of them, check out Android Jetpack Architecture Components: Getting Started.

Additionally, to learn more about testing architecture components, check out Testing Android Architecture Components.

To dig deeper into LiveData, read LiveData Tutorial for Android: Deep Dive.

For more information about architecture components, head to Architecture Components: Official Documentation.

I hope you enjoyed this tutorial on lifecycle-aware components. If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

3 ratings

More like this

Contributors

Comments