Android fundamentals 10.1 Part A: Room, LiveData, and ViewModel

1. Welcome

Introduction

Architecture Components logo

The Android operating system provides a strong foundation for building apps that run well on a wide range of devices and form factors. However, issues like complex lifecycles and the lack of a recommended app architecture make it challenging to write robust apps. The Android Architecture Components provide libraries for common tasks such as lifecycle management and data persistence to make it easier to implement the recommended architecture.

Architecture Components help you structure your app in a way that is robust, testable, and maintainable with less boilerplate code.

When it comes to architecture, it helps to see the big picture first. To introduce the terminology, here's a short overview of the Architecture Components and how they work together. Each component is explained more as you use it in this practical.

The diagram below shows a basic form of the recommended architecture for apps that use Architecture Components. The architecture consists of a UI controller, a ViewModel that serves LiveData, a Repository, and a Room database. The Room database is backed by an SQLite database and accessible through a data access object (DAO). Each component is described briefly below, and in detail in the Architecture Components concepts chapter, 10.1: Storing data with Room. You implement the components in this practical.

76fd05c075c6179c.png

Because all the components interact, you will encounter references to these components throughout this practical, so here is a short explanation of each.

Entity: In the context of Architecture Components, the entity is an annotated class that describes a database table.

SQLite database: On the device, data is stored in an SQLite database. The Room persistence library creates and maintains this database for you.

DAO: Short for data access object. A mapping of SQL queries to functions. You used to have to define these queries in a helper class. When you use a DAO, your code calls the functions, and the components take care of the rest.

Room database: Database layer on top of an SQLite database that takes care of mundane tasks that you used to handle with a helper class. The Room database uses the DAO to issue queries to the SQLite database based on functions called.

Repository: A class that you create for managing multiple data sources. In addition to a Room database, the Repository could manage remote data sources such as a web server.

ViewModel

: Provides data to the UI and acts as a communication center between the Repository and the UI. Hides the backend from the UI. ViewModel instances survive device configuration changes.

LiveData

: A data holder class that follows the observer pattern, which means that it can be observed. Always holds/caches latest version of data. Notifies its observers when the data has changed. Generally, UI components observe relevant data. LiveData is lifecycle-aware, so it automatically manages stopping and resuming observation based on the state of its observing activity or fragment.

What you should already know

You should be able to create and run apps in Android Studio 3.0 or higher. In particular, be familiar with:

It helps to be familiar with:

  • Software architectural patterns that separate data from the UI.
  • The observer pattern. In summary, the observer pattern defines a one-to-many dependency between objects. Whenever an object changes its state, all the object's dependents are notified and updated automatically. The main object is called the "subject" and its dependents are called the "observers." Usually, the subject notifies the observers by calling one of the observers' methods. The subject knows what methods to call, because the observers are "registered" with the subject and specify the methods to call.

What you'll learn

  • How to design and construct an app using some of the Android Architecture Components. You'll use Room, ViewModel, and LiveData.

What you'll do

  • Create an app with an Activity that displays words in a RecyclerView.
  • Create an Entity that represents word objects.
  • Define the mapping of SQL queries to Java methods in a DAO (data access object).
  • Use LiveData to make changes to the data visible to the UI, by way of observers.
  • Add a Room database to the app for persisting data locally, and initialize the database.
  • Abstract the data backend as a Repository class with an API that is agnostic to how the data is stored or acquired.
  • Use a ViewModel to separate all data operations from the UI.
  • Add a second Activity that allows the user to add new words.

2. App overview

In this practical you build an app that uses the Android Architecture Components. The app, called RoomWordsSample, stores a list of words in a Room database and displays the list in a RecyclerView. The RoomWordsSample app is basic, but sufficiently complete that you can use it as a template to build on.

The RoomWordsSample app does the following:

  • Works with a database to get and save words, and pre-populates the database with some words.
  • Displays all the words in a RecyclerView in MainActivity.
  • Opens a second Activity when the user taps the + FAB button. When the user enters a word, the app adds the word to the database and then the list updates automatically.

The screenshots below show the following:

  • The RoomWordsSample app as it starts, with the initial list of words
  • The activity to add a word

Snapshot of the RoomWordsSample app at startup

Snapshot of the RoomWordsSample app edit activity

RoomWordsSample architecture overview

The following diagram mirrors the overview diagram from the introduction and shows all the pieces of the RoomWordsSample app. Each of the enclosing boxes (except for the SQLite database) represents a class that you create.

51a4bfd20a93ebe5.png

3. Task 1: Create the RoomWordsSample app

1.1 Create an app with one Activity

Open Android Studio and create an app. On the setup screens, do the following:

  • Name the app RoomWordsSample.
  • If you see check boxes for Include Kotlin support and Include C++ support, uncheck both boxes.
  • Select only the Phone & Tablet form factor, and set the minimum SDK to API 14 or higher.
  • Select the Basic Activity.

Add a Basic Activity to your app

1.2 Update Gradle files

In Android Studio, manually add the Architecture Component libraries to your Gradle files.

  1. Add the following code to your build.gradle (Module: app) file, to the bottom of the dependencies block (but still inside it).
// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVersion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
  1. In your build.gradle (Project: RoomWordsSample) file, add the version numbers at the end of the file.
ext {
   roomVersion = '1.1.1'
   archLifecycleVersion = '1.1.1'
}

4. Task 2: Create the Word entity

The diagram below is the complete architecture diagram with the component that you are going to implement in this task highlighted. Every task will have such a diagram to help you understand where the current component fits into the overall structure of the app, and to see how the components are connected.

c6ef2260e6944814.png

The data for this app is words, and each word is represented by an entity in the database. In this task you create the Word class and annotate it so Room can create a database table from it. The diagram below shows a word_table database table. The table has one word column, which also acts as the primary key, and two rows, one each for "Hello" and "World."

8272047064aeb7ce.png

2.1 Create the Word class

  1. Create a class called Word.
  2. Add a constructor that takes a word string as an argument. Add the @NonNull annotation so that the parameter can never be null.
  3. Add a "getter" method called getWord()that returns the word. Room requires "getter" methods on the entity classes so that it can instantiate your objects.
public class Word {
   private String mWord;
   public Word(@NonNull String word) {this.mWord = word;}
   public String getWord(){return this.mWord;}
}

2.2 Annotate the Word class

To make the Word class meaningful to a Room database, you must annotate it. Annotations identify how each part of the Word class relates to an entry in the database. Room uses this information to generate code.

You use the following annotations in the steps below:

  • @Entity(tableName = "word_table") Each @Entity class represents an entity in a table. Annotate your class declaration to indicate that the class is an entity. Specify the name of the table if you want it to be different from the name of the class.
  • @PrimaryKey Every entity needs a primary key. To keep things simple, each word in the RoomWordsSample app acts as its own primary key. To learn how to auto-generate unique keys, see the tip below.
  • @NonNull Denotes that a parameter, field, or method return value can never be null. The primary key should always use this annotation. Use this annotation for any mandatory fields in your rows.
  • @ColumnInfo(name = "word") Specify the name of a column in the table, if you want the column name to be different from the name of the member variable.
  • Every field that's stored in the database must either be public or have a "getter" method. This app provides a getWord() "getter" method rather than exposing member variables directly.

For a complete list of annotations, see the Room package summary reference.

Update your Word class with annotations, as shown in the code below.

  1. Add the @Entity notation to the class declaration and set the tableName to "word_table".
  2. Annotate the mWord member variable as the @PrimaryKey. Require mWord to be @NonNull, and name the column "word".

Here is the complete code:

@Entity(tableName = "word_table")
public class Word {

   @PrimaryKey
   @NonNull
   @ColumnInfo(name = "word")
   private String mWord;

   public Word(@NonNull String word) {this.mWord = word;}

   public String getWord(){return this.mWord;}
}

If you get errors for the annotations, you can import them manually, as follows:

import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;

5. Task 3: Create the DAO

bfb7676f8fcfa5ba.png

The data access object, or Dao, is an annotated class where you specify SQL queries and associate them with method calls. The compiler checks the SQL for errors, then generates queries from the annotations. For common queries, the libraries provide convenience annotations such as @Insert.

Note that:

  • The DAO must be an interface or abstract class.
  • Room uses the DAO to create a clean API for your code.
  • By default, all queries (@Query) must be executed on a thread other than the main thread. (You work on that later.) For operations such as inserting or deleting, if you use the provided convenience annotations, Room takes care of thread management for you.

3.1 Implement the DAO class

The DAO for this practical is basic and only provides queries for getting all the words, inserting words, and deleting all the words.

  1. Create a new interface and call it WordDao.
  2. Annotate the class declaration with @Dao to identify the class as a DAO class for Room.
  3. Declare a method to insert one word:
void insert(Word word);
  1. Annotate the insert() method with @Insert. You don't have to provide any SQL! (There are also @ Delete and @ Update annotations for deleting and updating a row, but you do not use these operations in the initial version of this app.)
  2. Declare a method to delete all the words:
void deleteAll();
  1. There is no convenience annotation for deleting multiple entities, so annotate the deleteAll() method with the generic @Query. Provide the SQL query as a string parameter to @Query. Annotate the deleteAll() method as follows:
@Query("DELETE FROM word_table")
  1. Create a method called getAllWords() that returns a List of Words:
List<Word> getAllWords();
  1. Annotate the getAllWords() method with an SQL query that gets all the words from the word_table, sorted alphabetically for convenience:
@Query("SELECT * from word_table ORDER BY word ASC")

Here is the completed code for the WordDao class:

@Dao
public interface WordDao {

   @Insert
   void insert(Word word);

   @Query("DELETE FROM word_table")
   void deleteAll();

   @Query("SELECT * from word_table ORDER BY word ASC")
   List<Word> getAllWords();
}

To learn more about DAOs, see Accessing data using Room DAOs.

6. Task 4: Use LiveData

81993979f98bd230.png

When you display data or use data in other ways, you usually want to take some action when the data changes. This means you have to observe the data so that when it changes, you can react.

LiveData, which is a lifecycle library class for data observation, can help your app respond to data changes. If you use a return value of type LiveData in your method description, Room generates all necessary code to update the LiveData when the database is updated.

4.1 Return LiveData in WordDao

  • In the WordDao interface, change the getAllWords() method signature so that the returned List<Word> is wrapped with LiveData<>.
   @Query("SELECT * from word_table ORDER BY word ASC")
   LiveData<List<Word>> getAllWords();

See the LiveData documentation to learn more about other ways to use LiveData, or watch this Architecture Components: LiveData and Lifecycle video.

7. Task 5: Add a Room database

bea6a2db22efaf9c.png

Room is a database layer on top of an SQLite database. Room takes care of mundane tasks that you used to handle with a database helper class such as SQLiteOpenHelper.

  • Room uses the DAO to issue queries to its database.
  • By default, to avoid poor UI performance, Room doesn't allow you to issue database queries on the main thread. LiveData applies this rule by automatically running the query asynchronously on a background thread, when needed.
  • Room provides compile-time checks of SQLite statements.
  • Your Room class must be abstract and extend RoomDatabase.
  • Usually, you only need one instance of the Room database for the whole app.

5.1 Implement a Room database

  1. Create a public abstract class that extends RoomDatabase and call it WordRoomDatabase.
 public abstract class WordRoomDatabase extends RoomDatabase {}
  1. Annotate the class to be a Room database. Declare the entities that belong in the database—in this case there is only one entity, Word. (Listing the entities class or classes creates corresponding tables in the database.) Set the version number. Also set export schema to false, exportSchema keeps a history of schema versions. For this practical you can disable it, since you are not migrating the database.
@Database(entities = {Word.class}, version = 1, exportSchema = false)
  1. Define the DAOs that work with the database. Provide an abstract "getter" method for each @Dao.
 public abstract WordDao wordDao();
  1. Create the WordRoomDatabase as a singleton to prevent having multiple instances of the database opened at the same time, which would be a bad thing. Here is the code to create the singleton:
private static WordRoomDatabase INSTANCE;

public static WordRoomDatabase getDatabase(final Context context) {
   if (INSTANCE == null) {
       synchronized (WordRoomDatabase.class) {
           if (INSTANCE == null) {
               // Create database here
           }
       }
   }
   return INSTANCE;
}
  1. Add code to create a database where indicated by the Create database here comment in the code above.

The following code uses Room's database builder to create a RoomDatabase object named "word_database" in the application context from the WordRoomDatabase class.

// Create database here
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
       WordRoomDatabase.class, "word_database")
       .build();
  1. Add a migration strategy for the database.

In this practical you don't update the entities and the version numbers. However, if you modify the database schema, you need to update the version number and define how to handle migrations. For a sample app such as the one you're creating, destroying and re-creating the database is a fine migration strategy. For a real app, you must implement a non-destructive migration strategy. See Understanding migrations with Room.

Add the following code to the builder, before calling build()

// Wipes and rebuilds instead of migrating 
// if no Migration object.
// Migration is not part of this practical.
.fallbackToDestructiveMigration()

Here is the complete code for the whole WordRoomDatabase class:

@Database(entities = {Word.class}, version = 1, exportSchema = false)
public abstract class WordRoomDatabase extends RoomDatabase {

   public abstract WordDao wordDao();
   private static WordRoomDatabase INSTANCE;

   static WordRoomDatabase getDatabase(final Context context) {
       if (INSTANCE == null) {
           synchronized (WordRoomDatabase.class) {
               if (INSTANCE == null) {
                   INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                           WordRoomDatabase.class, "word_database")
                             // Wipes and rebuilds instead of migrating 
                             // if no Migration object.
                            // Migration is not part of this practical.
                           .fallbackToDestructiveMigration()
                           .build();                
               }
           }
       }
       return INSTANCE;
   }
}

8. Task 6: Create the Repository

cbbe89e4603663a0.png

A Repository is a class that abstracts access to multiple data sources. The Repository is not part of the Architecture Components libraries, but is a suggested best practice for code separation and architecture. A Repository class handles data operations. It provides a clean API to the rest of the app for app data.

Dao, Repo, network block diagram

A Repository manages query threads and allows you to use multiple backends. In the most common example, the Repository implements the logic for deciding whether to fetch data from a network or use results cached in the local database.

6.1 Implement the Repository

  1. Create a public class called WordRepository.
  2. Add member variables for the DAO and the list of words.
private WordDao mWordDao;
private LiveData<List<Word>> mAllWords;
  1. Add a constructor that gets a handle to the database and initializes the member variables.
WordRepository(Application application) {
    WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
    mWordDao = db.wordDao();
    mAllWords = mWordDao.getAllWords();
}
  1. Add a wrapper method called getAllWords() that returns the cached words as LiveData. Room executes all queries on a separate thread. Observed LiveData notifies the observer when the data changes.
LiveData<List<Word>> getAllWords() {
   return mAllWords;
}
  1. Add a wrapper for the insert() method. Use an AsyncTask to call insert() on a non-UI thread, or your app will crash. Room ensures that you don't do any long-running operations on the main thread, which would block the UI.
public void insert (Word word) {
    new insertAsyncTask(mWordDao).execute(word);
}
  1. Create the insertAsyncTask as an inner class. You should be familiar with AsyncTask, so here is the insertAsyncTask code for you to copy:
private static class insertAsyncTask extends AsyncTask<Word, Void, Void> {

    private WordDao mAsyncTaskDao;

    insertAsyncTask(WordDao dao) {
        mAsyncTaskDao = dao;
    }

    @Override
    protected Void doInBackground(final Word... params) {
        mAsyncTaskDao.insert(params[0]);
        return null;
    }
}

Here is the complete code for the WordRepository class:

public class WordRepository {

   private WordDao mWordDao;
   private LiveData<List<Word>> mAllWords;

   WordRepository(Application application) {
       WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
       mWordDao = db.wordDao();
       mAllWords = mWordDao.getAllWords();
   }

   LiveData<List<Word>> getAllWords() {
       return mAllWords;
   }

   public void insert (Word word) {
       new insertAsyncTask(mWordDao).execute(word);
   }

   private static class insertAsyncTask extends AsyncTask<Word, Void, Void> {

       private WordDao mAsyncTaskDao;

       insertAsyncTask(WordDao dao) {
           mAsyncTaskDao = dao;
       }

       @Override
       protected Void doInBackground(final Word... params) {
           mAsyncTaskDao.insert(params[0]);
           return null;
       }
   }
}

9. Task 7: Create the ViewModel

a9af4122219e140e.png

The ViewModel is a class whose role is to provide data to the UI and survive configuration changes. A ViewModel acts as a communication center between the Repository and the UI. The ViewModel is part of the lifecycle library. For an introductory guide to this topic, see ViewModel.

d92efc9a58fc4080.png

A ViewModel holds your app's UI data in a way that survives configuration changes. Separating your app's UI data from your Activity and Fragment classes lets you better follow the single responsibility principle: Your activities and fragments are responsible for drawing data to the screen, while your ViewModel is responsible for holding and processing all the data needed for the UI.

In the ViewModel, use LiveData for changeable data that the UI will use or display.

7.1 Implement the WordViewModel

  1. Create a class called WordViewModel that extends AndroidViewModel.

An Activity can be destroyed and created many times during the lifecycle of a ViewModel, such as when the device is rotated. If you store a reference to the Activity in the ViewModel, you end up with references that point to the destroyed Activity. This is a memory leak. If you need the application context, use AndroidViewModel, as shown in this practical. </div>

public class WordViewModel extends AndroidViewModel {}
  1. Add a private member variable to hold a reference to the Repository.
   private WordRepository mRepository;
  1. Add a private LiveData member variable to cache the list of words.
  private LiveData<List<Word>> mAllWords;
  1. Add a constructor that gets a reference to the WordRepository and gets the list of all words from the WordRepository.
   public WordViewModel (Application application) {
       super(application);
       mRepository = new WordRepository(application);
       mAllWords = mRepository.getAllWords();
   }
  1. Add a "getter" method that gets all the words. This completely hides the implementation from the UI.
   LiveData<List<Word>> getAllWords() { return mAllWords; }
  1. Create a wrapper insert() method that calls the Repository's insert() method. In this way, the implementation of insert() is completely hidden from the UI.
public void insert(Word word) { mRepository.insert(word); }

Here is the complete code for WordViewModel:

public class WordViewModel extends AndroidViewModel {

   private WordRepository mRepository;

   private LiveData<List<Word>> mAllWords;

   public WordViewModel (Application application) {
       super(application);
       mRepository = new WordRepository(application);
       mAllWords = mRepository.getAllWords();
   }

   LiveData<List<Word>> getAllWords() { return mAllWords; }

   public void insert(Word word) { mRepository.insert(word); }
}

To learn more, watch the Architecture Components: ViewModel video.

10. Task 8: Add XML layouts for the UI

Next, add the XML layout for the list and items to be displayed in the RecyclerView.

This practical assumes that you are familiar with creating layouts in XML, so the code is just provided.

8.1 Add styles

  1. Change the colors in colors.xml to the following: (to use Material Design colors):
<resources>
   <color name="colorPrimary">#2196F3</color>
   <color name="colorPrimaryLight">#64b5f6</color>
   <color name="colorPrimaryDark">#1976D2</color>
   <color name="colorAccent">#FFFF9800</color>
   <color name="colorTextPrimary">@android:color/white</color>
   <color name="colorScreenBackground">#fff3e0</color>
   <color name="colorTextHint">#E0E0E0</color>
</resources>
  1. Add a style for text views in the values/styles.xml file:
<style name="text_view_style">
   <item name="android:layout_width">match_parent</item>
   <item name="android:layout_height">wrap_content</item>
   <item name="android:textAppearance">
      @android:style/TextAppearance.Large</item>
   <item name="android:background">@color/colorPrimaryLight</item>
   <item name="android:layout_marginTop">8dp</item>
   <item name="android:layout_gravity">center</item>
   <item name="android:padding">16dp</item>
   <item name="android:textColor">@color/colorTextPrimary</item>
</style>

8.2 Add item layout

  • Add a layout/recyclerview_item.xml layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=
      "http://schemas.android.com/apk/res/android"
   android:orientation="vertical" android:layout_width="match_parent"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_height="wrap_content">

   <TextView
       android:id="@+id/textView"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       style="@style/text_view_style"
       tools:text="placeholder text" />
</LinearLayout>

8.3 Add the RecyclerView

  1. In the layout/content_main.xml file, add a background color to the ConstraintLayout:
android:background="@color/colorScreenBackground"
  1. In content_main.xml file, replace the TextView element with a RecyclerView element:
<android.support.v7.widget.RecyclerView
   android:id="@+id/recyclerview"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_margin="16dp"
   tools:listitem="@layout/recyclerview_item"
/>

8.4 Fix the icon in the FAB

The icon in your floating action button (FAB) should correspond to the available action. In the layout/activity_main.xml file, give the FloatingActionButton a + symbol icon:

  1. Select File > New > Vector Asset.
  2. Select Material Icon.
  3. Click the Android robot icon in the Icon: field, then select the + ("add") asset.
  4. In the layout/activity_main.xml file, in the FloatingActionButton, change the srcCompat attribute to:
android:src="@drawable/ic_add_black_24dp"

11. Task 9: Create an Adapter and adding the RecyclerView

You are going to display the data in a RecyclerView, which is a little nicer than just throwing the data in a TextView. This practical assumes that you know how RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder, and RecyclerView.Adapter work.

9.1 Create the WordListAdapter class

  • Add a class WordListAdapter that extends RecyclerView.Adapter. The adapter caches data and populates the RecyclerView with it. The inner class WordViewHolder holds and manages a view for one list item.

Here is the code:

public class WordListAdapter extends RecyclerView.Adapter<WordListAdapter.WordViewHolder> {

   private final LayoutInflater mInflater;
   private List<Word> mWords; // Cached copy of words

   WordListAdapter(Context context) { mInflater = LayoutInflater.from(context); }

   @Override
   public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
       View itemView = mInflater.inflate(R.layout.recyclerview_item, parent, false);
       return new WordViewHolder(itemView);
   }

   @Override
   public void onBindViewHolder(WordViewHolder holder, int position) {
       if (mWords != null) {
           Word current = mWords.get(position);
           holder.wordItemView.setText(current.getWord());
       } else {
           // Covers the case of data not being ready yet.
           holder.wordItemView.setText("No Word");
       }
   }

   void setWords(List<Word> words){
       mWords = words;
       notifyDataSetChanged();
   }

   // getItemCount() is called many times, and when it is first called,
   // mWords has not been updated (means initially, it's null, and we can't return null).
   @Override
   public int getItemCount() {
       if (mWords != null)
           return mWords.size();
       else return 0;
   }

   class WordViewHolder extends RecyclerView.ViewHolder {
       private final TextView wordItemView;

       private WordViewHolder(View itemView) {
           super(itemView);
           wordItemView = itemView.findViewById(R.id.textView);
       }
   }
}

9.2 Add RecyclerView to MainActivity

  1. Add the RecyclerView in the onCreate() method of MainActivity:
RecyclerView recyclerView = findViewById(R.id.recyclerview);
final WordListAdapter adapter = new WordListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
  1. Run your app to make sure the app compiles and runs. There are no items, because you have not hooked up the data yet. The app should display the empty recycler view.

because you have not hooked up the data yet. The app should display the empty recycler view.

12. Task 10: Populate the database

There is no data in the database yet. You will add data in two ways: Add some data when the database is opened, and add an Activity for adding words. Every time the database is opened, all content is deleted and repopulated. This is a reasonable solution for a sample app, where you usually want to restart on a clean slate.

10.1 Create the callback for populating the database

To delete all content and repopulate the database whenever the app is started, you create a RoomDatabase.Callback and override the onOpen() method. Because you cannot do Room database operations on the UI thread, onOpen() creates and executes an AsyncTask to add content to the database.

  1. Add the onOpen() callback in the WordRoomDatabase class:
private static RoomDatabase.Callback sRoomDatabaseCallback = 
    new RoomDatabase.Callback(){

    @Override
    public void onOpen (@NonNull SupportSQLiteDatabase db){
        super.onOpen(db);
       new PopulateDbAsync(INSTANCE).execute();
   }
};
  1. Create an inner class PopulateDbAsync that extends AsycTask. Implement the doInBackground() method to delete all words, then create new ones. Here is the code for the AsyncTask that deletes the contents of the database, then populates it with an initial list of words. Feel free to use your own words!
/**
* Populate the database in the background.
*/
private static class PopulateDbAsync extends AsyncTask<Void, Void, Void> {

   private final WordDao mDao;
   String[] words = {"dolphin", "crocodile", "cobra"};

   PopulateDbAsync(WordRoomDatabase db) {
       mDao = db.wordDao();
   }

   @Override
   protected Void doInBackground(final Void... params) {
       // Start the app with a clean database every time.
       // Not needed if you only populate the database
       // when it is first created
       mDao.deleteAll();

       for (int i = 0; i <= words.length - 1; i++) {
           Word word = new Word(words[i]);
           mDao.insert(word);
       }
       return null;
   }
}
  1. Add the callback to the database build sequence in WordRoomDatabase, right before you call .build():
.addCallback(sRoomDatabaseCallback)

13. Task 11: Connect the UI with the data

Now that you have created the method to populate the database with the initial set of words, the next step is to add the code to display those words in the RecyclerView.

To display the current contents of the database, you add an observer that observes the LiveData in the ViewModel. Whenever the data changes (including when it is initialized), the onChanged() callback is invoked. In this case, the onChanged() callback calls the adapter's setWord() method to update the adapter's cached data and refresh the displayed list.

11.1 Display the words

  1. In MainActivity, create a member variable for the ViewModel, because all the activity's interactions are with the WordViewModel only.
private WordViewModel mWordViewModel;
  1. In the onCreate() method, get a ViewModel from the ViewModelProviders class.
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);

Use ViewModelProviders to associate your ViewModel with your UI controller. When your app first starts, the ViewModelProviders class creates the ViewModel. When the activity is destroyed, for example through a configuration change, the ViewModel persists. When the activity is re-created, the ViewModelProviders return the existing ViewModel. See ViewModel.

  1. Also in onCreate(), add an observer for the LiveData returned by getAllWords(). When the observed data changes while the activity is in the foreground, the onChanged() method is invoked and updates the data cached in the adapter. Note that in this case, when the app opens, the initial data is added, so onChanged() method is called.
mWordViewModel.getAllWords().observe(this, new Observer<List<Word>>() {
   @Override
   public void onChanged(@Nullable final List<Word> words) {
       // Update the cached copy of the words in the adapter.
       adapter.setWords(words);
   }
});
  1. Run the app. The initial set of words appears in the RecyclerView.

Initial set of words in the RecyclerView

14. Task 12: Create an Activity for adding words

Now you will add an Activity that lets the user use the FAB to enter new words. This is what the interface for the new activity will look like:

Interface for adding a new word

12.1 Create the NewWordActivity

  1. Add these string resources in the values/strings.xml file:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
  1. Add a style for buttons in value/styles.xml:
<style name="button_style" parent="android:style/Widget.Material.Button">
   <item name="android:layout_width">match_parent</item>
   <item name="android:layout_height">wrap_content</item>
   <item name="android:background">@color/colorPrimaryDark</item>
   <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
   <item name="android:layout_marginTop">16dp</item>
   <item name="android:textColor">@color/colorTextPrimary</item>
</style>
  1. Use the Empty Activity template to create a new activity, NewWordActivity. Verify that the activity has been added to the Android Manifest.
<activity android:name=".NewWordActivity"></activity>
  1. Update the activity_new_word.xml file in the layout folder:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@color/colorScreenBackground"
   android:orientation="vertical"
   android:padding="24dp">

   <EditText
       android:id="@+id/edit_word"
       style="@style/text_view_style"
       android:hint="@string/hint_word"
       android:inputType="textAutoComplete" />

   <Button
       android:id="@+id/button_save"
       style="@style/button_style"
       android:text="@string/button_save" />
</LinearLayout>
  1. Implement the NewWordActivity class. The goal is that when the user presses the Save button, the new word is put in an Intent to be sent back to the parent Activity.

Here is the code for the NewWordActivity activity:

public class NewWordActivity extends AppCompatActivity {
   public static final String EXTRA_REPLY = 
             "com.example.android.roomwordssample.REPLY";

   private  EditText mEditWordView;

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_new_word);
       mEditWordView = findViewById(R.id.edit_word);

       final Button button = findViewById(R.id.button_save);
       button.setOnClickListener(new View.OnClickListener() {
           public void onClick(View view) {
               Intent replyIntent = new Intent();
               if (TextUtils.isEmpty(mEditWordView.getText())) {
                   setResult(RESULT_CANCELED, replyIntent);
               } else {
                   String word = mEditWordView.getText().toString();
                   replyIntent.putExtra(EXTRA_REPLY, word);
                   setResult(RESULT_OK, replyIntent);
               }
               finish();
           }
       });
   }
}

12.2 Add code to insert a word into the database

  1. In MainActivity, add the onActivityResult() callback for the NewWordActivity. If the activity returns with RESULT_OK, insert the returned word into the database by calling the insert() method of the WordViewModel.
public void onActivityResult(int requestCode, int resultCode, Intent data) {
   super.onActivityResult(requestCode, resultCode, data);

   if (requestCode == NEW_WORD_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
       Word word = new Word(data.getStringExtra(NewWordActivity.EXTRA_REPLY));
       mWordViewModel.insert(word);
   } else {
       Toast.makeText(
               getApplicationContext(),
               R.string.empty_not_saved,
               Toast.LENGTH_LONG).show();
   }
}
  1. Define the missing request code:
public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;
  1. In MainActivity,start NewWordActivity when the user taps the FAB. Replace the code in the FAB's onClick() click handler with the following code:
Intent intent = new Intent(MainActivity.this, NewWordActivity.class);
startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE);
  1. Run your app. When you add a word to the database in NewWordActivity, the UI automatically updates.
  2. Add a word that already exists in the list. What happens? Does your app crash? Your app uses the word itself as the primary key, and each primary key must be unique. You can specify a conflict strategy to tell your app what to do when the user tries to add an existing word.
  3. In the WordDao interface, change the annotation for the insert() method to:
@Insert(onConflict = OnConflictStrategy.IGNORE)

To learn about other conflict strategies, see the OnConflictStrategy reference.

  1. Run your app again and try adding a word that already exists. What happens now?

15. Solution code

Android Studio project: RoomWordsSample

16. Summary

Now that you have a working app, let's recap what you've built. Here is the app structure again, from the beginning:

  • You have an app that displays words in a list (MainActivity, RecyclerView, WordListAdapter).
  • You can add words to the list (NewWordActivity).
  • A word is an instance of the Word entity class.
  • The words are cached in the RecyclerViewAdapter as a List of words (mWords). The list is automatically updated and redisplayed when the data changes.
  • The automatic update happens because in the MainActivity, there is an Observer that observes the words and is notified when the words change. When there is a change, the observer's onChange() method is executed and updates mWords in the WordListAdapter.
  • The data can be observed because it is LiveData. And what is observed is the LiveData<List<Word>> that is returned by the WordViewModel object.
  • The WordViewModel hides everything about the backend from the user interface. It provides methods for accessing the UI data, and it returns LiveData so that MainActivity can set up the observer relationship. Views, activities, and fragments only interact with the data through the ViewModel. As such, it doesn't matter where the data comes from.
  • In this case, the data comes from a Repository. The ViewModel does not need to know what that Repository interacts with. It just needs to know how to interact with the Repository, which is through the methods exposed by the Repository.
  • The Repository manages one or more data sources. In the RoomWordsSample app, that backend is a Room database. Room is a wrapper around and implements an SQLite database. Room does a lot of work for you that you used to have to do yourself. For example, Room does everything that you used to use an SQLiteOpenHelper class to do.
  • The DAO maps method calls to database queries, so that when the Repository calls a method such as getAllWords(), Room can execute SELECT * from word_table ORDER BY word ASC.
  • The result returned from the query is observed LiveData. Therefore, every time the data in Room changes, the Observer interface's onChanged() method is executed and the UI is updated.

17. Related concept

The related concept documentation is in 10.1: Room, LiveData, and ViewModel.

18. Learn more

To continue working with the RoomWordsSample app and learn more ways to use a Room database, see the 10.1 Part B: Room, LiveData, and ViewModel codelab, which takes up where this codelab leaves off.

Android developer documentation:

Blogs and articles:

Codelab:

Videos:

Code samples:

19. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

Create an app that uses a Room database, ViewModel, and LiveData to display the data when the data changes. You can make this as simple or as sophisticated as you wish, as long as the app uses all the required components, and the data updates on the screen when the data changes in the database.

Here are some hints and ideas:

  • Create a simple app that stores one text document and displays the contents of the document in a TextView. When the user edits the document, changes appear in the TextView.
  • Create a question-answer app. Start with only questions and let users add new questions and answers.
  • As a challenge, add a button to each answer in the question-answer app that displays additional information that's stored in a different repository. The information could come from a file on the device, or from a page on the internet.

Answer these questions

Question 1

What are the advantages of using a Room database?

  • Creates and manages an Android SQLite database for you.
  • Eliminates a lot of boilerplate code.
  • Helps you manage multiple backends.
  • Using a DAO, provides a mechanism for mapping Java methods to database queries.

Question 2

Which of the following are reasons for using a ViewModel?

  • Cleanly separates the UI from the backend.
  • Often used with LiveData for changeable data that the UI will use or display.
  • Prevents your data from being lost when the app crashes.
  • Acts as a communication center between the Repository and the UI.
  • ViewModel instances survive device configuration changes.

Question 3

What is the DAO?

  • Short for "data access object."
  • A library for managing database queries.
  • An annotated interface that maps Java methods to SQLite queries.
  • A class whose methods run always in the background, not on the main thread.
  • A class that the compiler checks for SQL errors, then uses to generate queries from the annotations.

Question 4

What are features of LiveData?

  • When LiveData is used with Room, data updates automatically if all the intermediate levels also return LiveData (DAO, ViewModel, Repository).
  • Uses the observer pattern and notifies its observers when its data has changed.
  • Automatically updates the UI when it changes.
  • Is lifecycle aware.

Submit your app for grading

Guidance for graders

Check that the app has the following features:

  • Uses the Room database to store data.
  • Uses ViewModel and LiveData to manage and display changing data.
  • Has a way to edit data and show changes.

20. Next codelab

To find the next practical codelab in the Android Developer Fundamentals (V2) course, see Codelabs for Android Developer Fundamentals (V2).

For an overview of the course, including links to the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).