This practical codelab is part of Unit 4: Saving user data in the Android Developer Fundamentals (Version 2) course. You will get the most value out of this course if you work through the codelabs in sequence:

Introduction

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.

What are the recommended Architecture Components?

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.

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:

What you'll learn

What you'll do

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:

The screenshots below show the following:

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.

1.1 Create an app with one Activity

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

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'
}

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.

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."

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:

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;

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:

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.

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

   @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.

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.

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;
   }
}

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.

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;
       }
   }
}

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.

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.

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

<?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"

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

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.

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)

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.

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:

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?

Android Studio project: RoomWordsSample

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

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

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:

Codelabs:

Videos:

Code samples:

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:

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:

Answer these questions

Question 1

What are the advantages of using a Room database?

Question 2

Which of the following are reasons for using a ViewModel?

Question 3

What is the DAO?

Question 4

What are features of LiveData?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

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).