Components

Architecture components are a set of Android libraries that help you structure your app in a way that is robust, testable, and maintainable.

This codelab introduces you to the following lifecycle-aware architecture components for building Android apps:

What you'll build

In this codelab, you implement examples of each of the components described above. You begin with a sample app and add code through a series of steps, integrating the various architecture components as you progress.

What you'll need

In this step, you download the code for the entire codelab and then run a simple example app.

$ git clone git@github.com:googlecodelabs/android-lifecycles.git

Alternatively you can download the repository as a Zip file:

Download source code

Once you have the code:

  1. Open the project Android Studio version 2.5 or newer.
  2. Copy over the Android Studio run configurations that will be used in this codelab. From the code root directory:


MacOS / Linux:

mkdir -p .idea/runConfigurations
cp runConfigurations/* .idea/runConfigurations/

Windows:

MKDIR .idea\runConfigurations
COPY runConfigurations\* .idea\runConfigurations\

Once you have run the command, you should see run configurations for each step:

  1. Run the Step 1 run configuration on a device or emulator.

The app runs and displays a screen similar to the following screenshot:

  1. Rotate the screen and notice that the timer resets!

You now need to update the app to persist state across screen rotations. You can use a ViewModel because instances of this class survive configuration changes, such as screen rotation.

In this step, you use a ViewModel to persist state across screen rotations and address the behaviour you observed in the previous step. In the previous step, you ran an activity that displays a timer. This timer is reset when a configuration change, such as screen rotation, destroys an activity.

You can use a ViewModel to retain data across the entire lifecycle of an activity or a fragment. As the previous step demonstrates, an activity is a poor choice to manage app data. Activities and fragments are short-lived objects which are created and destroyed frequently as a user interacts with an app. A ViewModel is also better suited to managing tasks related to network communication, as well as data manipulation and persistence.

Persisting the state of the Chronometer using a ViewModel

Open ChronoActivity2 and examine how the class retrieves and uses a ViewModel:

ChronometerViewModel chronometerViewModel
                = new ViewModelProvider(this).get(ChronometerViewModel.class);

this refers to an instance of LifecycleOwner. The framework keeps the ViewModel alive as long as the scope of the LifecycleOwner is alive. A ViewModel is not destroyed if its owner is destroyed for a configuration change, such as screen rotation. The new instance of the owner re-connects to the existing ViewModel, as illustrated by the following diagram:

Try it out

Run the app (choose Step 2 in the Run Configurations dropdown) and confirm the timer doesn't reset when you perform either of the following actions:

  1. Rotate the screen.
  2. Navigate to another app and then return.

However, if you or the system exit the app, then the timer resets.

In this step, you replace the chronometer used in previous steps with a custom one which uses a Timer, and updates the UI every second. A Timer is a java.util class that you can use to schedule recurrent tasks. You add this logic to the LiveDataTimerViewModel class, and leave the activity to focus on managing the interaction between the user and the UI.

The activity updates the UI when notified by the timer. To help avoid memory leaks, the ViewModel doesn't include references to the activity. For example, a configuration change, such as a screen rotation, might result in references in a ViewModel to an activity that should be garbage collected. The system retains instances of ViewModel until the corresponding activity or lifecycle owner no longer exists.

Instead of modifying views directly from the ViewModel, you configure an activity or fragment to observe a data source, receiving the data when it changes. This arrangement is called the observer pattern.

You may be familiar with the observer pattern if you've used the Data Binding Library, or other reactive libraries like RxJava. LiveData is a special observable class which is lifecycle-aware, and only notifies active observers.

LifecycleOwner

ChronoActivity3 is an instance of AppCompatActivity which is a subclass of ComponentActivity. Take a look at the reference documentation and notice that ComponentActivity implements LifecycleOwner. LifecycleOwner is an interface that is used by any class that has an Android lifecycle. Practically, it means that the class has a Lifecycle object that represents its lifecycle.

Both ViewModel and LiveData can bind to a Lifecycle.

Update ChronoActivity

1. Add the following code to the ChronoActivity3 class, in the subscribe() method, to create the subscription:

mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver);

2. Next, set the new elapsed time value in the LiveDataTimerViewModel class. Find the following comment:

//TODO post the new value with LiveData.postValue()

Replace the comment with the following statement:

mElapsedTime.postValue(newValue);

3. Run the app and open Logcat in Android Studio. Notice that the log updates every second unless you navigate to another app. If your device supports multi-window mode, you may like to try using it. Rotating the screen does not affect how the app behaves.

Many Android components and libraries require you to:

  1. Subscribe, or initialize the component or library.
  2. Unsubscribe, or stop the component or library.

Failing to complete the steps above can lead to memory leaks and subtle bugs.

A lifecycle owner object can be passed to new instances of lifecycle-aware components, to ensure they're aware of the current state of a lifecycle.

You can query the current state of a lifecycle using the following statement:

lifecycleOwner.getLifecycle().getCurrentState()

The statement above returns a state, such as Lifecycle.State.RESUMED, or Lifecycle.State.DESTROYED.

A lifecycle-aware object that implements LifecycleObserver can also observe changes in the state of a lifecycle owner:

lifecycleOwner.getLifecycle().addObserver(this);

You can annotate the object to instruct it to call the appropriate methods when required:

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
void addLocationListener() { ... }

Create a lifecycle-aware component

In this step, you create a component that reacts to an activity lifecycle owner. Similar principles and steps apply when using a fragment as the lifecycle owner.

You use the Android framework's LocationManager to get the current latitude and longitude and display them to the user. This addition allows you to:

You would typically subscribe a LocationManager to changes in either the onStart() or onResume() methods of an activity, and remove the listener in the onStop() or onPause() methods:

// Typical use, within an activity.

@Override
protected void onResume() {
    mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mListener);
}

@Override
protected void onPause() {
    mLocationManager.removeUpdates(mListener);
}

In this step, you'll update a class called BoundLocationManager to be lifecycle-aware: it will bind to, observe and react to changes in a LifecycleOwner.

For the class to observe the activity's lifecycle, you must add it as an observer. To accomplish this, instruct the BoundLocationManager object to observe the lifecycle by adding the following code to its constructor:

lifecycleOwner.getLifecycle().addObserver(this);

To call a method when a lifecycle change occurs, you can use the @OnLifecycleEvent annotation. Update the addLocationListener() and removeLocationListener() methods with the following annotations in the BoundLocationListener class:

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
void addLocationListener() {
    ...
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void removeLocationListener() {
    ...
}

Run the app and verify that the Log Monitor displays the following actions, when you rotate the device:

D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed
D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed

Use the Android Emulator to simulate changing the location of the device (click the three dots to show the extended controls). The TextView is updated when it changes:

Share a ViewModel between fragments

Complete the following additional steps using a ViewModel to enable communication between fragments and the following:

Run this step (Select run "Step 5" in run configurations) and notice two instances of SeekBar which are independent of each other:

Connect the fragments with a ViewModel so that when one SeekBar is changed, the other SeekBar is updated:

There's no step-by-step manual for this exercise but you can find a solution in the step5_solution package.

From Overview of memory management:

As the system runs low on memory, it kills processes in the cache beginning with the process least recently used. When the user navigates back to the app, the system will restart the app in a new process.

Since this only happens if the user has not interacted with the app for a while, it might be permissible to have them return to the app and find it in the initial state. However, there are cases where you might want to save the state of the app or part of it, so that that information is not lost if the process happens to be killed.

The lifecycle-viewmodel-savedstate module provides access to the saved state in ViewModels.

The gradle dependency for this module is

"androidx.lifecycle:lifecycle-viewmodel-savedstate:$savedStateVersion"

Once you have the dependency, as long as you are using a default Fragment or Activity, you'll have access to a SavedStateHandle in your ViewModel. A SavedStateHandle is a key-value mapping that survives process death.

First, let's try out step 6 without these changes:

1. Open run configuration "Step 6"

You will see a simple form:

2. Change the name and click "SAVE". This will store it in a LiveData inside the ViewModel.

3. Simulate the system killing the process (requires emulator running P+). First make sure the process is running by typing:

$ adb shell ps -A |grep lifecycle

This should output the running process with name com.example.android.codelabs.lifecycle

Press Home on your device or emulator and then run:

$ adb shell am kill com.example.android.codelabs.lifecycle

If you then type again

$ adb shell ps -A |grep lifecycle

You should get nothing, indicating that the process has been killed correctly.

4. Open the app again (Look for LC Step6 in your app launcher).

The value in the ViewModel was not persisted however the EditText restored its state. How is this possible?

Actually, the lifecycle-viewmodel-savedstate module also uses onSaveInstanceState and onRestoreInstanceState to persist the ViewModel state but it makes these operations more convenient.

Implement a saved state for ViewModels

In the SavedStateViewModel.java file you need to add a new constructor that takes a SavedStateHandle and store the state in a private field:

private SavedStateHandle mState;

public SavedStateViewModel(SavedStateHandle savedStateHandle) {
   mState = savedStateHandle;
}

Now you'll use the module's LiveData support so you don't need to store and expose a LiveData in your ViewModel anymore. Replace the existing getter and saveNewName with:

private static final String NAME_KEY = "name";

// Expose an immutable LiveData
LiveData<String> getName() {
    return mState.getLiveData(NAME_KEY);
}

void saveNewName(String newName) {
    mState.set(NAME_KEY, newName);
}

Now that you're using LiveData from mState, the MutableLiveData name isn't used anymore and can be removed.

Now you can try the same process again. Open the app, change the name and save it. Then, press Home and kill the process with:

$ adb shell am kill com.example.android.codelabs.lifecycle

If you reopen the app, you'll see that the state in the ViewModel has been saved this time.

With the SavedStateHandler you can save and restore primitives, Bundles, Parcelables, Serializables and other types of data. Check out the Saved State module for ViewModel documentation for all the details.

All rights reserved. Java is a registered trademark of Oracle and/or its affiliates.