Categories Architecture Components

Properly manage lifecycles with Lifecycle-Aware Components

11 Apr. 2019
2
0
26 minutes
Lifecycle-aware components

In 2018, Android introduced Android Jetpack, a new collection of Android software components whose purpose is to help developers build great Android apps in less time. Jetpack offers developers guidance and tools to simplify complex tasks, eliminate boilerplate code and build high-quality apps. It accelerates development by facilitating a modern app architecture. and provides robust backwards compatibility.

Android Jetpack

Jetpack components are arranged in four categories:

  • Architecture components: Facilitate highly testable, robust and maintainable app architecture, while individually addressing developer pain points such as data persistence or lifecycle management.
  • Behavior components: Include support for standard Android services such as notifications, permissions, Slices, and sharing.
  • Foundation components: Includes support for backwards compatibility, automated testing and new Kotlin language.
  • User Interface components: Provide widgets and helpers to make your apps ergonomic and delightful.
Android Jetpack components

Each Jetpack components are individually adaptable but built to work together. As these components are modular, developers can pick relevant Jetpack components and put them together in their apps to offer a great experience, whether for the developer or for users.

Note that Jetpack comprises the androidx.* package libraries which offers backward compatibility. This package is updated more frequently than the Android platform.

Architecture components are a collection of libraries that help developers build testable, robust and maintainable apps. It provides developers with classes to easily manage UI component lifecycle and handle data persistence. The key principles of Architecture components are:

  • Separation of concerns: Decomposition of a computer program into parts that overlap in functionality as little as possible.
  • Loose coupling: Loose coupling describes a relationship between two objects that can interact with each other but have very limited knowledge about what the other object can do.
  • Observer pattern: Design pattern in which an object, called the subject or the observable, maintains a list of other objects, called observers, that observe it and get notified automatically when the state of the subject changes.
  • Inversion of Control: Delegation of the flow of control in a computer program to underlying libraries.

Here is a list of Architecture components:

  • Data Binding: Declaratively bind observable data to UI elements.
  • Lifecycles: Manage your activity and fragment lifecycles.
  • LiveData: Observable data holder that notifies views when underlying data changes.
  • Navigation: Handle everything needed for in-app navigation.
  • Paging: Gradually load information on demand from your data source.
  • Room: Provide access to app’s SQLite database with in-app objects and compile-time checks.
  • ViewModel: Manage UI-related data in a lifecycle-conscious way. Stored data isn’t destroyed on app rotations.
  • WorkManager: Manage Android background jobs.

This course focuses on lifecycle management with Lifecycle-Aware Components. It includes Lifecycles, LiveData and ViewModel. Lifecycle-Aware Components perform actions in response to changes in the lifecycle status of another component such as an activity or a fragment.

When a change happens in the lifecycle status of another component, actions are usually implemented in the lifecycle of these components (Activity lifecycle and Fragment lifecycle). However, it is a bad practice because it leads to a poor organization and to the proliferation of errors.

Lifecycle-Aware Components solve that problem by handling outsourced actions from components. It contributes to greater reusability, maintainability and robustness.

In this course we will study Lifecycle-Aware Components through various examples.

Lifecycle

When a user navigates through an app, components that are contained in the app get through different states corresponding to stages of their lifecycle. In Android, Lifecycle is a class that defines an object that has a lifecycle. It holds information about the lifecycle state of a component and allows other objects to observe this state.

For instance, AppCompatActivity and Fragment implement LifecycleOwner interface which includes the getLifecycle() method to access the Lifecycle. Therefore, it is possible to subscribe components to these objects owners to observe changes in their lifecycle.

Lifecycle relies on two main enumerations to track the lifecycle status of a component:

  • State: Stage in the lifecycle of a component.
  • Event: Event that make a component transit from one state to another.
Lifecycle states and events

Project setup

Given that Architecture Components are available from Google’s Maven repository, you must add google() repository in your project-level build.gradle file.

allprojects {
    repositories {
        google()
        jcenter()
    }
}

Then add the dependencies for the ViewModel and LiveData in the module-level build.gradle file of your project.

dependencies {
    def lifecycle_version = "2.0.0"

    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}
dependencies {
    def lifecycle_version = "2.0.0"

    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}

Let’s take a simple example of an application that displays the elapsed time since it was started. The layout would look like this:

<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:padding="@dimen/padding_large">

    <Chronometer
            android:id="@+id/chronometer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="60sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

The chronometer is started in onCreate() method of the related activity.

chronometer.start();
Chronometer chronometer = findViewById(R.id.chronometer);
chronometer.start();

Now you have a nice chronometer in the center of your screen.

Chronometer demonstration

Unfortunately, it is reset when a configuration change happens, such as an activity destruction or screen rotations. This is where the ViewModel comes in.

ViewModel

The ViewModel class is designed to manage UI-related data in a lifecycle-conscious way. It can retain data across the entire lifecycle of a LifecycleOwner (activity or fragment). As a result, ViewModel objects are automatically retained during configuration changes such as an activity destruction or screen rotations. Therefore ViewModel data is immediately available to the next activity or fragment instance.

The following diagram shows the lifecycle of an Activity which undergoes a rotation and then is finally finished.

Lifecycle of ViewModel and Activity

As activities and fragments are frequently created, stopped, resumed, destroyed and so on, it has become difficult for developers to retain data state through configuration changes. A ViewModel provides an appropriate response to this problem.

Let’s get back to the example in hand. We would like to retain the state of the chronometer on configuration changes. In order to achieve this, let’s create a ViewModel that retains the start time of the chronometer.

class ChronometerViewModel : ViewModel() {
    var startTime: Long? = null
}
public class ChronometerViewModel extends ViewModel {
    private Long startTime;

    public Long getStartTime() {
        return startTime;
    }

    public void setStartTime(Long startTime) {
        this.startTime = startTime;
    }
}

It simply extends ViewModel and define a long value alongside its getter and setter.

Note that a ViewModel must never reference a view or any class that may hold a reference to the activity context. Indeed, components that hold a context may be destroyed, recreated, destroyed again and so on. Consequently, a reference to such a class may result in a memory leak so that’s a bad idea.

If the ViewModel needs theApplication context, it can extend the AndroidViewModel class and have a constructor that receives the Application in the constructor, since Application class extends Context.

The next step consists in associating this ViewModel with our LifecycleOwner, our activity MainActivity in other words. ViewModelProviders.of(< Activity or Fragment >).get(< Custom ViewModel>.class) is the provided method for that purpose. The first method, of() creates a ViewModelProvider, which retains the custom ViewModel you provided while a scope of given Activity or Fragment is alive. The second method, get() returns an existing ViewModel or creates a new one in the scope (usually, a fragment or an activity), associated with this ViewModelProvider.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel::class.java)

    // Start date of the chronometer
    val startTime: Long

    if (chronometerViewModel.startTime == null) {
        // New ViewModel
        startTime = SystemClock.elapsedRealtime()
        chronometerViewModel.startTime = startTime
    } else {
        // Existing ViewModel
        startTime = chronometerViewModel.startTime!!
    }

    chronometer.base = startTime
    chronometer.start() 
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Chronometer chronometer = findViewById(R.id.chronometer);

    ChronometerViewModel chronometerViewModel
            = ViewModelProviders.of(this).get(ChronometerViewModel.class);

    // Start date of the chronometer
    long startTime;

    if (chronometerViewModel.getStartTime() == null) {
        // New ViewModel
        startTime = SystemClock.elapsedRealtime();
        chronometerViewModel.setStartTime(startTime);
    } else {
        // Existing ViewModel
        startTime = chronometerViewModel.getStartTime();
    }

    chronometer.setBase(startTime);
    chronometer.start();
}

And that’s it! Now the chronometer isn’t reset on configuration change, either it is a screen rotation or an app change. However, it is reset when you, or the system, kill the app.

Now imagine it’s a timed multiplayer quiz app that is started by the server. How would we proceed to start the chronometer only after the server gave us its consent? A decent solution would be to observe some kind of data that the server shares in order to let us know when the chronometer can start. That’s when LiveData comes in.

LiveData

LiveData is an observable data holder class that is lifecycle-aware. Observer objects can observe a LiveData object and get notified when the data changes. As LiveData is lifecycle-aware, it ensures that it only updates observers that are in an active lifecycle state.

Using LiveData provides many advantages. It ensures your User Interface matches your data state by keeping it up to date. Furthermore, it handles lifecycle and configuration changes properly. Therefore, it avoids some crashes and memory leaks.

LiveData has no publicly available methods to update the stored data. The MutableLiveData class extends LiveData and exposes the setValue(T) and postValue(T) to edit the wrapped data. You must be wondering what is the difference between these two methods and it is simple. setValue(T) sets the value and dispatch the value to all the active observers. However, it can’t be called from a background thread. That’s the purpose of postValue(T) which posts a task to the main thread to set the given value.

LiveData is a wrapper that can be used with any data class. For instance, it could be a Boolean, a List or a custom class. In our case, we will simulate the signal of the server that would tell us we can start the chronometer. We will simply define a MutableLiveData object that wraps a Boolean. This flag will be updated three seconds after the creation of the activity to emulate the server signal.

It would look like this:

class ChronometerViewModel : ViewModel() {

    var startTime: Long? = null
    val canChronometerStart = MutableLiveData<Boolean>()

    init {
        // Delay to simulate an asynchronous response from the server
        Handler().postDelayed({ canChronometerStart.postValue(true) }, THREE_SECONDS.toLong())
    }

    companion object {

        private const val THREE_SECONDS = 3000
    }
}
public class ChronometerViewModel extends ViewModel {

    private static final int THREE_SECONDS = 3000;

    private Long startTime;
    private MutableLiveData<Boolean> canChronometerStart = new MutableLiveData<>();

    public ChronometerViewModel() {
        // Delay to simulate an asynchronous response from the server
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                canChronometerStart.postValue(true);
            }
        }, THREE_SECONDS);
    }

    public Long getStartTime() {
        return startTime;
    }

    public void setStartTime(Long startTime) {
        this.startTime = startTime;
    }

    public MutableLiveData<Boolean> getCanChronometerStart() {
        return canChronometerStart;
    }
}

Once that’s done, we need to observe this flag to start the chronometer at the right time. In order to achieve this, we need to attach an Observer object to the MutableLiveData object using the  observe(LifecycleOwner owner, Observer<T> observer) method. This method adds an observer to the observers list within the lifespan of the given LifecycleOwner. The Observer must define the onChanged() method, which controls what happens when the LiveData object’s data changes.

Let’s attach an observer to our MutableLiveData object and start the chronometer.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel::class.java)

    chronometerViewModel.canChronometerStart.observe(this,
        Observer<Boolean> { canChronometerStart ->
            if (canChronometerStart!!) {
                // Start date of the chronometer
                val startTime: Long

                if (chronometerViewModel.startTime == null) {
                    // New ViewModel
                    startTime = SystemClock.elapsedRealtime()
                    chronometerViewModel.startTime = startTime
                } else {
                    // Existing ViewModel
                    startTime = chronometerViewModel.startTime!!
                }

                chronometer.base = startTime

                chronometer.start()
            }
        })
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final Chronometer chronometer = findViewById(R.id.chronometer);

    final ChronometerViewModel chronometerViewModel
            = ViewModelProviders.of(this).get(ChronometerViewModel.class);


    chronometerViewModel.getCanChronometerStart().observe(this, new Observer<Boolean>() {
        @Override
        public void onChanged(Boolean canChronometerStart) {
            if(canChronometerStart) {
                // Start date of the chronometer
                long startTime;

                if (chronometerViewModel.getStartTime() == null) {
                    // New ViewModel
                    startTime = SystemClock.elapsedRealtime();
                    chronometerViewModel.setStartTime(startTime);
                } else {
                    // Existing ViewModel
                    startTime = chronometerViewModel.getStartTime();
                }

                chronometer.setBase(startTime);

                chronometer.start();
            }
        }
    });
}

Note that the observer is notified after the delay and the chronometer starts at that time. When there is a configuration change, the observer is also notified so the chronometer is resumed with the stored time.

Create a lifecycle-aware component

Many Android components and libraries require to be initialized and stopped during the lifecycle of its owner. A bad handling of one of these steps can lead to crashes and memory leaks. An elegant solution would be to define a lifecycle-aware component that reacts to the lifecycle of a LifecycleOwner. It can be achieved by creating a component that implements LifecycleObserver. Such a component can observe changes in the state of a lifecycle owner and performs related actions.

You can annotate a method with @OnLifecycleEvent(Lifecycle.EVENT.<event>) to call it when the specified event happens. Here is a list of events you can bind methods to:

The example below shows a component that reacts to an activity lifecycle owner.

class CustomObserver(lifecycleOwner: LifecycleOwner) : LifecycleObserver {

    init {
        lifecycleOwner.lifecycle.addObserver(this)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun connectListener() {
        // Initialization for instance
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun disconnectListener() {
        // Terminate
    }
}
public class CustomObserver implements LifecycleObserver {

    public CustomObserver(LifecycleOwner lifecycleOwner) {
        lifecycleOwner.getLifecycle().addObserver(this);
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void connectListener() {
        // Initialization for instance
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void disconnectListener() {
        // Terminate
    }
}

Note that our custom Observer is added to the list of the LifecycleOwner‘s observers using lifecycleOwner.getLifecycle().addObserver(this). Then you can create an instance of this component as follows:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    CustomObserver(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    new CustomObserver(this);
}

Share a ViewModel between Fragments

A ViewModel can be easily shared between several Fragments by providing their activity to ViewModelProviders.of(<Activity>). This way the custom ViewModel will be retained while a scope of the Activity. The Github project whose link is at the bottom of this pots includes a ViewModel shared between two simple fragments that have an EditText. When a user types on an EditText, the other will also be changed.

Shared Viewmodel between fragments

Download

  • Kotlin
  • Java

Conclusion

To recap, Architecture components are a collection of libraries that help developers build testable, robust and maintainable apps. Lifecycle, LiveData and ViewModel are part of these components and they fit well together to improve your overall architecture. Using such components prevents some crashes and memory leaks and it allows the user not to worry about configuration changes. More generally, these Architecture components enhance the quality of your app. This post is the first post of a long series of posts related to Jetpack components. Most of Jetpack components will be studied.

Leave a Reply

Your email address will not be published. Required fields are marked *