Using Android Architecture Components: Lifecycles and SQLite made easy


Before Google I/O 2017, Google avoided recommending any particular architecture for Android development. You could use Model View Presenter (MVP), Model View Controller (MVC), Model-View-ViewModel (MVVM), some other pattern, or even no pattern at all.

With the release of the Android Architecture Components project, we finally have an official recommendation for Android Application Architecture, and all the components we need to implement it.

The Android Architecture Components project spans a number of libraries, but since Room and Lifecycles recently saw their first stable release, I’ll focus on how to set up and use both of these production-ready libraries.

By the end of this tutorial, you should know how to avoid memory leaks and application crashes by creating components that are capable of managing their own lifecycles, and how to store data locally in SQLite using much less boilerplate code.

>> An SQLite primer for Android app developers

Creating lifecycle-aware components

Most Android components have a lifecycle associated with them. You used to be responsible for managing your application’s lifecycle, which wasn’t always easy, especially if your application featured multiple asynchronous calls happening simultaneously.

Failing to manage application lifecycles correctly can result in all kinds of issues, ranging from memory leaks, to your application losing the user’s progress, and even outright application crashes.

Even if you managed the lifecycle correctly, implementing all of that lifecycle-dependent code in your lifecycle methods (such as onStart and onStop) could make them bloated and complex. This makes methods difficult to read, maintain, and test.

The Lifecycles library offers a new approach to lifecycle management, by providing all the classes and interfaces needed to create lifecycle-aware components that adjust their behavior automatically, and even perform different actions in response to lifecycle events.

As of Support Library 26.1.0, Fragments and Activities have a Lifecycle object attached to them, which lets them broadcast their lifecycle state to other components within your application. For example, if an Activity features video content, then you can use this new lifecycle awareness to detect and pause the video automatically whenever the Activity loses focus, and restart it as soon as your application regains the foreground.

Since this process is automated, Lifecycles helps you avoid many problems caused by lifecycle mismanagement. Since you’re not cramming all of that lifecycle-dependent code into your lifecycle methods, your code is going to be much more organized, so it’ll be easier to read, maintain, and test.

To use the Lifecycles library, you need to add the following to your module-level build.gradle file:

dependencies {
   implementation "android.arch.lifecycle:runtime:1.0.3"
   annotationProcessor "android.arch.lifecycle:compiler:1.0.0"

If you want to use Java 8.0 with the Lifecycles library, then you’ll also need to add the following:

implementation "android.arch.lifecycle:common-java8:1.0.0"

The Lifecycles library introduces the following components:

  • Lifecycle – An abstract class that has an Android Lifecycle attached to it. Objects can observe this state and act accordingly.
  • LifecycleOwner – An interface that’s implemented by objects with a Lifecycle. Fragments and Activities already implement the LifecycleOwner interface (in Support Library 26.1.0+), and are therefore LifecycleOwners by default. You can observe LifecycleOwners— and any class that extends a LifecycleOwner— using a LifecycleObsever.
  • LifecycleObserver – LifecycleObserver receives updates about LifecycleOwner events. Prior to the Lifecycles library, you could only react to methods that were triggered by lifecycle events, like onCreate and onDestroy, but now you can create methods that are triggered by changes in a LifecycleOwner’s state. You can make a method lifecycle-aware by adding the @OnLifecycleEvent annotation.
  • Observer – An Observer receives an update whenever their assigned LifecycleOwner enters a new lifecycle state. An Observer that’s assigned to an Activity will be notified when this Activity enters a paused state, and again when it enters a resumed state. You add an Observer to a lifecycle, using lifecycle.addObserver(this).

The @OnLifecycleEvent annotation can react to the following lifecycle events:

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

ON_ANY is triggered by any lifecycle event. If you use Lifecycle.Event.ON_ANY, then the method should expect a LifecycleOwner and Lifecycle.Event argument.

Let’s look at how you’d create a LifecycleObserver that responds to changes in an Activity’s state. In the following code, we’re printing a message to Android Studio’s Logcat whenever the associated LifecycleOwner (MainActivity) enters a started or stopped state:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleObserver;
import android.util.Log;
import android.arch.lifecycle.OnLifecycleEvent;

public class MainActivity extends AppCompatActivity {
   private static final String TAG = "MainActivity";
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

//Associate the LifecycleOwner with the LifecycleObserver, using getLifecycle().addObserver//

       getLifecycle().addObserver(new Observer());
   }

//Create an Observer that implements the LifecycleObserver interface//

   public class Observer implements LifecycleObserver {

//Use @OnLifecycleEvent to listen for different lifecycle events//

       @OnLifecycleEvent(Lifecycle.Event.ON_START)
       public void onStart() {
           Log.e(TAG, "ON_START");
       }

       @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
       public void onStop() {
           Log.e(TAG, "ON_STOP");
       }
 
       }
   }

You can also handle multiple lifecycle events within the same method:

@OnLifecycleEvent({ON_STOP, ON_START})

If your project already has methods that handle lifecycle events, you can add the @OnLifecycleEvent annotation to these existing methods, rather than re-writing your current implementation.

Performing operations based on the Lifecycle state

You’ll usually only want to perform an operation when a Lifecycle is in a certain state.

For example, if you attempt to perform a FragmentTransaction after an Activity state has been saved, then the FragmentManager is going to throw an exception. By using getState.isAtLeast, you can ensure this operation only happens when the Lifecycle is in a compatible state:

public void startFragmentTransaction() {
     if (lifecycle.getState.isAtLeast(STARTED)) {
//Perform the transaction//
     }
  }

The isAtLeast method can check for the following Lifecycle states:

  • INITIALIZED.
  • CREATED.
  • STARTED.
  • RESUMED.
  • DESTROYED.

You can also retrieve the current lifecycle state by calling getCurrentState().

LiveData: Keep track of changing data

The Lifecycles library also provides the foundation for additional Android Architecture Components, including LiveData, which you can use alongside the Room data persistence library.

LiveData is an observable wrapper that can hold any data, including Lists. Once a LiveData has a value, it’ll notify its assigned Observers whenever that value changes.

However, unlike a regular Observable, LiveData is lifecycle-aware, so it only updates Observers that are in an “active” state (i.e STARTED or RESUMED). If the LifecycleOwner reaches a Lifecycle.State.DESTROYED state, then the LiveData will remove the Observer automatically. This lifecycle-awareness helps you avoid the crashes and errors that can occur if you try to update a stopped Activity or Fragment.

Whenever a component enters the STARTED state, it automatically receives the most recent value from the LiveData object it’s observing. If an Activity or Fragment is resumed, or recreated as part of a configuration change, then it’ll receive the latest data.

To use the LiveData component in your project, you’ll need to add the following to your module-level build.gradle file:

implementation "android.arch.lifecycle:extensions:1.0.0"

Easier data storage with Room

If your app handles a significant amount of structured data, you can often improve the user experience by caching some of this data locally. If you persist all of your app’s most important data then users will be able to continue using your application, even when they don’t have an internet connection.

While Android has supported the SQLite data persistence solution since version 1.0, the built-in APIs are fairly low-level and implementing them requires a significant amount of time, effort, and boilerplate code. There’s also no compile-time verification of raw SQL queries, and if the data changes then you’ll need to manually update your SQL queries to reflect these changes, which can be time-consuming and error-prone.

Room is an SQLite mapping library which aims to lower the barrier to using SQLite on Android. Room abstracts some of the underlying implementation details of creating and managing an SQLite database and lets you query data without having to use Cursors or Loaders. Room also provides compile-time validation of queries and entities, and forces you to perform database operations on a background thread, so you don’t inadvertently wind up blocking Android’s all-important main UI thread.

To use Room, add the following to your project’s module-level build.gradle file:

compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"

In Room, operations such as Insert, Update and Delete are annotated, which is why we need to add the annotation processor as a project dependency.

There are three major Room components:

1. Database

The @Database class provides the bridge between your application and SQLite.

Your @Database class must be an abstract class that extends RoomDatabase, which defines tables present in your database, provides Data Access Objects (DAO) classes, and includes a list of entities associated with the database.

//List the entities contained in your database. Separate multiple entities with a comma//

@Database(entities = {List.class}, version = 1)

//Define an abstract class that extends RoomDatabase//

public abstract class MyDatabase extends RoomDatabase {

//Declare your DAO(s) as abstract methods//

   public abstract ItemDao itemDao();
}

You can acquire an instance of Database by calling Room.databaseBuilder() or Room.inMemoryDatabaseBuilder().

2. Entity

Room creates a table for each class that you annotate with @Entity, where each field corresponds to a column in the table. Entity classes are usually small model classes that don’t contain any logic.

Room can only persist fields it has access to, so you either need to make a field public, or provide getter and setter methods. Each entity also needs to define at least one field as a primary key. Even if there’s only a single field, you’ll still need to annotate that field with @PrimaryKey.

@Entity

//Unless we specify otherwise, the table name will be List//

public class List {

    @PrimaryKey

//The column name will be id//

    private int id;

private String item;

  public String getItem() {
       return item;
   }

   public void setItem(String item) {
       this.item = item;
   }

Room uses the class name as the database table name, unless you override it using the tableName property:

@Entity(tableName = "list")

Room also derives the column name from the field name, unless you explicitly define the column name using the @ColumnInfo(name = “column_name”) annotation, for example:

@ColumnInfo(name = "productName")

Room creates a column for each field that’s defined in the entity. If there’s a field you don’t want to persist, then you’ll need to annotate it with @Ignore.

@Entity
public class List {
...
...
...

    @Ignore
    Bitmap image;
}

Even though most object-relational mapping libraries let you map relationships from a database to the respective object model, Room doesn’t allow object references. The reasoning behind this restriction is that this type of lazy loading typically occurs on Android’s main UI thread, which can result in unresponsive user interfaces and application crashes. Instead, you’ll need to explicitly request the data your app requires.

3. DAO

You access your data via Data Access Objects (DAO), which can either be an interface or an abstract class, but must contain all the methods you want to use in your database queries.

These annotated methods generate the corresponding SQL at compile time, reducing the amount of boilerplate you need to write and maintain.

There’s several convenience queries you can use with DAOs, including:

  • @Insert. When you annotate a DAO method with @Insert, Room generates an implementation inserting all entries into the database in a single transaction.
  • @Update. Modifies entities in the database.
  • @Delete. Removes entities from the database.
  • @Query. This is the main annotation you’ll use in your DAO classes and it’s how you’ll perform all your read/write operations.

The following DAO interface contains various operations that we can perform on our table:

@Dao
public interface ItemDao {

//We don’t need to write all that Cursor related code; instead, define queries using annotation//

@Query("SELECT * FROM List")
   List<List> fetchAllData();

   @Update
   void update(List list);

   @Delete
   void delete(List list);
}

Each @Query method is checked against the table schemas at compile time. If there’s a problem with a query, you’ll get a compilation error rather than a runtime failure.

When performing queries, you’ll often want your application to update automatically when the data changes. You can achieve this by using Room in combination with LiveData – specifically, by using a return value of type LiveData in your query method. Room will then generate all the code necessary to update the LiveData when the database is updated.

@Query("SELECT * FROM List")
LiveData<List<List>> fetchAllData();

Room and RxJava

If you’re using the RxJava library in your project,  you can create Room queries that return a backpressure-aware Flowable. Flowable is a new addition to RxJava 2.0 that helps you avoid the issue of a source Observable emitting items too quickly for the downstream Observer to process. This can result in a backlog of unconsumed, memory-hogging items.

To use RxJava with Room, you’ll need to add the following dependency to your module-level build.gradle file:

implementation "android.arch.persistence.room:rxjava2:1.0.0"

If you’re interested in learning more about using RxJava 2.0 in combination with the Room library, then Google has published a Room and RxJava Sample app.

Wrapping up

Even though Room is Google’s recommended approach for working with databases, Android still supports direct database access using SQLite, so you don’t need to switch to Room.

Are you happy with Android’s default SQLite implementation? Will you be migrating to Room? Let us know in the comments!