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

The new Room Persistence Library guarantee SQL statements at compile-time and removes boilerplate code related to storing data locally in SQLite using the following features:

What you'll build

In this codelab, you begin with a sample app and add code through a series of steps, integrating the various persistence 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.

Click the following button to download all the code for this codelab.

Download source code

  1. Unzip the code, and then open the project using Android Studio version 2.3 or newer.
  2. Run the Step 1 run configuration to make sure there are no errors or missing dependencies.

If your app handles non-trivial amounts of data, you can gain a number of benefits by storing at least some of that data locally using Room. The most common benefit is to optimize network connectivity, by caching relevant data to ensure users can still browse that content while they are offline. Any changes the user makes to content can later be synced to the server, once the device is back online.

The core Android framework provides built-in support for working with raw SQL data. While the built-in APIs are very powerful, they also present a number of development challenges:

Room is designed to abstract away the underlying database tables and queries, and encourage best-practice development patterns on Android.

By default, Room doesn't allow you to issue database queries on the main thread to avoid poor UI performance. However querying on the main thread is enabled in this codelab for simplicity.

Introducing the sample app

In the remainder of this codelab, you work on a library app that keeps track of book loans to users. The code creates an in-memory database with three tables: books, users, and loans, as well as some sample data.

Take a few minutes to explore the package db to learn about the entities, data access objects, database, and sample data you use later in the codelab. Open and inspect the following three files:

  1. The User data class.
  2. The Book data class.
  3. The Loan data class.

Notice that the @Entity annotation to mark a data class as a persistable entity. At least one of the class fields must be annotated with the @PrimaryKey annotation.

Next, open and inspect the following 3 files:

  1. The UserDao interface.
  2. The BookDao interface.
  3. The LoanDao interface.

The @Dao annotation is used to create data access objects, which define SQLite queries. You explore data access objects in more detail later in this codelab.

Finally, review the AppDatabase class, which is annotated using @Database. The AppDatabase class establishes a logical grouping between the UserDao, BookDao, and LoanDao data access object interfaces. The AppDatabase class also defines the required version number, which is used to track and implement database migrations.

A Data Access Object (DAO) is an abstract class or interface that includes methods to define database queries. The annotated methods in this class are used to generate the corresponding SQL at compile time. This abstraction helps to reduce the amount of repetitive boilerplate code you need to maintain. Unlike runtime SQL, these annotated methods are parsed and validated at compile time. For example, the following interface method defines a SQL query that takes a String with a username and returns a list of books loaned to that user:

@Query("SELECT * FROM Book " +
       "INNER JOIN Loan ON Loan.book_id == Book.id " +
       "WHERE Loan.user_id == :userId "
)
public List<Book> findBooksBorrowedByUser(String userId);

The following method illustrates another example, and it's used to insert books into the database. The method replaces the existing record if a conflict occurs:

@Insert(onConflict = REPLACE)
void insertBook(Book book);

Open the UserDao class and review some examples of the different types of queries you can define in a DAO:

Now try to define a new query, or modify an existing one, to introduce invalid SQL and rebuild the project. You should observe an error similar to the following message:

Error:(77, 23) error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: userId)

Next, open the step1 package and review the UsersActivity class, which fetches data from the database using the following code:

private void fetchData() {
    StringBuilder sb = new StringBuilder();
    List<User> youngUsers = mDb.userModel().loadAllUsers();
    for (User youngUser : youngUsers) {
        sb.append(String.format("%s, %s (%d)\n",
            youngUser.lastName, youngUser.name, youngUser.age));
    }
    mYoungUsersTextView.setText(sb);
}

Run the app, and notice there's now an impostor in the list:

Create a new @Query in the UserDao class which retrieves the list of users below a specified age:

@Query("SELECT * FROM User WHERE age < :age")
List<User> findYoungerThan(int age);

Next, call the new method from the activity, to only fetch young users:

 List<User> youngUsers = mDb.userModel().findYoungerThan(35);

Now run the app again and notice that the query retrieves the expected results:

There are a number of relationships that you can use to define links between entities:

In this step, you use an entity to define a many-to-many relationship called Loan to record when a user borrows and returns a book. You update the app to show a list of books that were borrowed by user Mike.

Review the following annotated query from db.BookDao.java:

  @Query("SELECT * FROM Book " +
            "INNER JOIN Loan ON Loan.book_id = Book.id " +
            "INNER JOIN User on User.id = Loan.user_id " +
            "WHERE User.name LIKE :userName"
    )
    public List<Book> findBooksBorrowedByNameSync(String userName);

Next, run the app and review the list of books borrowed by user Mike.

You might notice there's a performance problem; the UI is blocked while the database is being populated. This operation is purposely slow, to simulate a worst-case scenario.

The database is currently populated synchronously, so begin by removing the following code in JankShowUserActivity.java:

DatabaseInitializer.populateSync(mDb);

Replace the code you removed with the following statement, to switch to using an asynchronous task to populate the data:

 DatabaseInitializer.populateAsync(mDb);

Now run the app and review the list of books borrowed by user Mike as you did earlier in this step. You should find that no books are listed:

Take a moment to see if you can identify the cause of this behaviour.

The database is being populated in the background, but you're still querying it in the main thread. The issue is caused by a race condition. Query results arrive before the Loan table is populated. If you press REFRESH after a couple of seconds, the results appear:

There are two problems with this approach:

One way to solve the issue you observed might be to add a callback or listener to delay loading data until the database is populated. However, adding too many callbacks can complicate development and debugging. A more elegant solution would be to subscribe to, and observe changes in the database. You do this in the next step using lifecycle-aware components.

In this step, you integrate the code you added earlier in the codelab with LiveData, a lifecycle-aware component which can be observed, typically described as an observable. Simply wrapping your @Query return type with LiveData provides you with database observers for minimal extra effort. You also move the reference to the database, from the activity, to a ViewModel.

  1. Navigate to db.BookDao.java and create a method that does the same thing as findBooksBorrowedByNameSync() but wraps the return type in a LiveData object.
  2. Navigate to and review, step3.BooksBorrowedByUserViewModel. The BooksBorrowedByUserViewModel class exposes the list of books wrapped in a LiveData object, and is also responsible for creating and populating the database:
 class BooksBorrowedByUserViewModel extends ViewModel {

    public final LiveData<List<Book>> books;
...
}
  1. Instead of retrieving the list of books from the database, retrieve the LiveData object from the DAO and use it to subscribe to changes using the name field. Update step3.BooksBorrowedByUserViewModel.java to include the following code in the constructor:
// books is a LiveData object so updates are observed.
books = mDb.bookModel().findBooksBorrowedByName("Mike");
  1. Navigate to step3.BooksBorrowedByUserActivity and review how the ViewModel is provided in onCreate:
mViewModel = ViewModelProviders.of(this).get(BooksBorrowedByUserViewModel.class);
  1. Next, complete the subscribeUiBooks method by subscribing the activity to changes in the ViewModel object's list of books,:
private void subscribeUiBooks() {
    mViewModel.books.observe(this, new Observer<List<Book>>() {
        @Override
        public void onChanged(@NonNull final List<Book> books) {
            showBooksInUi(books, mBooksTextView);
        }
    });
}

The UI will now update smoothly whenever a loan record is added.

You can find the solution to this step in the step3_solution package.

Using the same activity from the previous step, you create a new @Query in the BookDao class to list the books borrowed by a user within the last day.

You can annotate the methods of arbitrary class using the @TypeConverter annotation. You can use type converters to define conversions between data types in your plain old Java object (POJO), and column types in a SQLite database. You can reference annotated classes from different scopes using the @TypeConverters annotation, for example from a @Database, @DAO, or @Entity, as illustrated by the following code samples:

public class DateConverter {
    @TypeConverter
    public Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }

    @TypeConverter
    public Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}
// Dao
@Entity
@TypeConverters(DateConverter.class)
class MyEntity {
    Date birthday;
}

In this step, you display the list of books that were recently returned. In BookDao create a @Query similar to findBooksBorrowedByName, which also accepts a date.

@Query("SELECT * FROM Book " +
        "INNER JOIN Loan ON Loan.book_id = Book.id " +
        "INNER JOIN User on User.id = Loan.user_id " +
        "WHERE User.name LIKE :userName " +
        "AND Loan.endTime > :after "
)
public LiveData<List<Book>> findBooksBorrowedByNameAfter(String userName, Date after);

The type is automatically converted using the DateConverter type converter.

Finally, modify the step4.TypeConvertersViewModel to use this query. Delete the following statement:

mBooks = mDb.bookModel().findBooksBorrowedByName("Mike");

Then, replace the statement you deleted above with the following code:

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DATE, -1);
Date yesterday = calendar.getTime();
mBooks = mDb.bookModel().findBooksBorrowedByNameAfter("Mike", yesterday);

Run the app and verify that it only displays three titles, as one of the books was returned the previous week:

You can view the source code for the DatabaseInitializer class for additional details on what initial data is being inserted and how initialization is implemented.

This last step is a challenge that you should try to solve on your own. Create a query and modify the ViewModel and the activity to show something similar to this screen:

This screen shows the returned date of each loan. Your query needs to fetch loans with information from both Users and Books.

Open the LoanWithUserAndBook class, and notice that it's simply a POJO.

public class LoanWithUserAndBook {
    public String id;
    @ColumnInfo(name="title")
    public String bookTitle;
    @ColumnInfo(name="name")
    public String userName;
    @TypeConverters(DateConverter.class)
    public long startTime;
    @TypeConverters(DateConverter.class)
    public long endTime;
}

This POJO can be used as a @Query return type. The fields returned from the query must match the fields in the class. The POJO is verified at compile time, and raises a warning if there is a mismatch.

Transformations

This step uses a class called Transformations that includes two very valuable functions:

They are very useful for cases where you need to transform data before exposing it or when data might not be always ready.

You can find a solution in the step5_solution package.

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