14.1A: Working with Architecture Components: Room, LiveData, ViewModel
Contents:
- What you should already KNOW
- What you will LEARN
- What you will DO
- App overview
- Task 1. Creating the RoomWordsSample app
- Task 2. Creating the Word entity
- Task 3. Creating the DAO
- Task 4. Using LiveData
- Task 5. Adding a Room database
- Task 6. Creating the Repository
- Task 7. Creating the ViewModel
- Task 8. Adding XML layouts for the UI
- Task 9. Creating an Adapter and adding the RecyclerView
- Task 10. Populating the database
- Task 11. Connecting the UI with the data
- Task 12. Adding an Activity for entering words
- Solution code
- Summary
- Related concept
- Learn more
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. 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 a SQLite database and accessible through a data access object (DAO). Each component is described briefly below, in detail in the Architecture Components concept chapter, and you will implement them 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 a 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 a 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:
RecyclerView
and adapters- SQLite databases and the SQLite query language
- Threading in general, and
AsyncTask
in particular - It helps to be familiar with software architectural patterns that separate data from the UI.
- It helps to be familiar with the observer pattern. In summary, the observer pattern defines a one-to-many dependency between objects, so that whenever an object changes its state, all its 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 their methods. The subjects knows what methods to call, because the observers are "registered" with the subject and specify the methods to call.
What you will LEARN
You will learn how to:
- Design and construct an app using some of the Android Architecture Components. You'll use Room,
ViewModel
, andLiveData
.
What you will DO
- Create an app with an
Activity
that displays words in aRecyclerView
. - 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 via 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.
App overview
In this practical you build an app that uses the Android Architecture Components collection of libraries. 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
inMainActivity
. - 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
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.
Task 1. Creating 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.
1.2 Update Gradle files
In Android Studio, manually add the Architecture Component libraries to your Gradle files.
Add the following code to your
build.gradle (Module: app)
file, to the bottom of the 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"
- In your
build.gradle (Project: RoomWordsSample)
file, add the version numbers at the end of the file.ext { roomVersion = '1.0.0' archLifecycleVersion = '1.1.0' }
Important: Get the latest version numbers from the Adding Components to your Project page. Find the entry for the room and lifecycle libraries and the version number is at the end of the name after the colon. In the following example, the version numbers is 1.0.0: "android.arch.persistence.room:runtime:
1.0.0
"
Task 2. Creating 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.
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
- Create a class called
Word
. - Add a constructor that takes a
word
string as an argument. Add the@NonNull
annotation so that the parameter can never benull
. - 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 benull
. 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.
- Add the
@Entity
notation to the class declaration and set thetableName
to"word_table"
. - Annotate the
mWord
member variable as the@PrimaryKey
. RequiremWord
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;
autoGenerate=true
. See Defining data using Room entities.
Task 3. Creating the DAO
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
orabstract
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.
- Create a new
interface
and call itWordDao
. - Annotate the class declaration with
@Dao
to identify the class as a DAO class for Room. - Declare a method to insert one word:
void insert(Word word);
- 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.) - Declare a method to delete all the words:
void deleteAll();
- 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 thedeleteAll()
method as follows:@Query("DELETE FROM word_table")
- Create a method called
getAllWords()
that returns aList
ofWords
:List<Word> getAllWords();
Annotate the
getAllWords()
method with a SQL query that gets all the words from theword_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(); }
Tip: For this app, ordering the words is not strictly necessary. However, by default, return order is not guaranteed, and ordering makes testing straightforward.
To learn more about DAOs, see Accessing data using Room DAOs.
Task 4. Using LiveData
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 thegetAllWords()
method signature so that the returnedList<Word>
is wrapped withLiveData<>
.
See the@Query("SELECT * from word_table ORDER BY word ASC") LiveData<List<Word>> getAllWords();
LiveData
documentation to learn more about other ways to useLiveData
, or watch this Architecture Components: LiveData and Lifecycle video.
Task 5. Adding a Room database
Room is a database layer on top of a 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 extendRoomDatabase
. - Usually, you only need one instance of the Room database for the whole app.
5.1 Implement a Room database
- Create a
public abstract
class that extendsRoomDatabase
and call itWordRoomDatabase
.public abstract class WordRoomDatabase extends RoomDatabase {}
- 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 theentities
class or classes creates corresponding tables in the database.) Set the version number.@Database(entities = {Word.class}, version = 1)
- Define the DAOs that work with the database. Provide an abstract "getter" method for each
@Dao
.public abstract WordDao wordDao();
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; }
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
RoomDatabas
e
object named"word_database"
in the application context from theWordRoomDatabase
class.// Create database here INSTANCE = Room.databaseBuilder(context.getApplicationContext(), WordRoomDatabase.class, "word_database") .build();
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) 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; } }
Important: In Android Studio, if you get errors when you paste code or during the build process, make sure you are using the full package name for imports. See Adding Components to your Project. Then select Build > Clean Project. Then select Build > Rebuild Project, and build again.
Task 6. Creating the Repository
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
- Create a public class called
WordRepository
. - Add member variables for the DAO and the list of words.
private WordDao mWordDao; private LiveData<List<Word>> mAllWords;
- 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(); }
- Add a wrapper method called
getAllWords()
that returns the cached words asLiveData
. Room executes all queries on a separate thread. ObservedLiveData
notifies the observer when the data changes.LiveData<List<Word>> getAllWords() { return mAllWords; }
- Add a wrapper for the
insert()
method. Use anAsyncTask
to callinsert()
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); }
Create the
insertAsyncTask
as an inner class. You should be familiar withAsyncTask
, so here is theinsertAsyncTask
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; } } }
Note: For this simple example, the Repository doesn't do much. For a more complex implementation, see the BasicSample code on GitHub.
Task 7. Creating the ViewModel
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
Create a class called
WordViewModel
that extendsAndroidViewModel
.Warning: Never pass context intoViewModel
instances. Do not storeActivity
,Fragment
, orView
instances or theirContext
in theViewModel
.
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 {}
- Add a private member variable to hold a reference to the Repository.
private WordRepository mRepository;
- Add a private
LiveData
member variable to cache the list of words.private LiveData<List<Word>> mAllWords;
- Add a constructor that gets a reference to the
WordRepository
and gets the list of all words from theWordRepository
.public WordViewModel (Application application) { super(application); mRepository = new WordRepository(application); mAllWords = mRepository.getAllWords(); }
- Add a "getter" method that gets all the words. This completely hides the implementation from the UI.
LiveData<List<Word>> getAllWords() { return mAllWords; }
Create a wrapper
insert()
method that calls the Repository'sinsert()
method. In this way, the implementation ofinsert()
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.
Task 8. Adding 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
- Change the colors in
colors.xml
to the following: (to use a set of 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>
- 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
- In the
layout/content_main.xml
file, add a background color to theConstraintLayout
:android:background="@color/colorScreenBackground"
- In
content_main.xml
file, replace theTextView
element with aRecyclerView
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:
- Select File > New > Vector Asset.
- Select Material Icon.
- Click the Android robot icon in the Icon: field, then select the
+
("add") asset. - In the
layout/activity_main.xml
file, in theFloatingActionButton
, change thesrcCompat
attribute to:.android:src="@drawable/ic_add_black_24dp"
Task 9. Creating 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
class that extendsRecyclerView.Adapter
. The adapter caches data and populates theRecyclerView
with it. The inner classWordViewHolder
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);
}
}
}
mWords
variable in the adapter caches the data. In the next task, you add the code that updates the data automatically.
getItemCount()
method needs to account gracefully for the possibility that the data is not yet ready and mWords
is still null
. In a more sophisticated app, you could display placeholder data or something else that would be meaningful to the user.
9.2 Add RecyclerView to MainActivity
- Add the
RecyclerView
in theonCreate()
method ofMainActivity
:RecyclerView recyclerView = findViewById(R.id.recyclerview); final WordListAdapter adapter = new WordListAdapter(this); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(this));
- 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.
Task 10. Populating 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.
If you need a refresher on AsyncTask
, see AsyncTaks and AsyncTaskLoader in the Android Fundamentals course.
Add the
onOpen()
callback in theWordRoomDatabase
class:private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback(){ @Override public void onOpen (@NonNull SupportSQLiteDatabase db){ super.onOpen(db); new PopulateDbAsync(INSTANCE).execute(); } };
Create an inner class
PopulateDbAsync
that extendsAsycTask
. Implement thedoInBackground()
method to delete all words, then create new ones. Here is the code for theAsyncTask
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; } }
- Add the callback to the database build sequence in
WordRoomDatabase
, right before you call.build()
:.addCallback(sRoomDatabaseCallback)
Task 11. Connecting 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
- In
MainActivity
, create a member variable for theViewModel
, because all the activity's interactions are with theWordViewModel
only.private WordViewModel mWordViewModel;
In the
onCreate()
method, get aViewModel
from theViewModelProviders
class.mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
Use
ViewModelProviders
to associate yourViewModel
with your UI controller. When your app first starts, theViewModelProviders
creates theViewModel
. When the activity is destroyed, for example through a configuration change, theViewModel
persists. When the activity is re-created, theViewModelProviders
return the existingViewModel
. SeeViewModel
.Also in
onCreate()
, add an observer for theLiveData
returned bygetAllWords()
. When the observed data changes while the activity is in the foreground, theonChanged()
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, soonChanged()
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); } });
- Run the app. The initial set of words appears in the
RecyclerView
.
Task 12. Creating 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:
12.1 Create the NewWordActivity
- 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>
- 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>
- 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>
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>
- Implement the
NewWordActivity
class. The goal is that when the user presses the Save button, the new word is put in anIntent
to be sent back to the parentActivity
.
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
In
MainActivity
, add theonActivityResult()
callback for theNewWordActivity
. If the activity returns withRESULT_OK
, insert the returned word into the database by calling theinsert()
method of theWordViewModel
.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(); } }
- Define the missing request code:
public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;
- In
MainActivity,
startNewWordActivity
when the user taps the FAB. Replace the code in the FAB'sonClick()
click handler with the following code:Intent intent = new Intent(MainActivity.this, NewWordActivity.class); startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE);
- RUN YOUR APP! When you add a word to the database in
NewWordActivity
, the UI automatically updates. - 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.
In the
WordDao
interface, change the annotation for theinsert()
method to:@Insert(onConflict = OnConflictStrategy.IGNORE)
To learn about other conflict strategies, see the
OnConflictStrategy
reference.Run your app again and try adding a word that already exists. What happens now?
Solution code
Android Studio project: RoomWordsSample.
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 aList
of words (mWords
). The list is automatically updated and redisplayed when the data changes. - The automatic update happens because in the
MainActivity
, there is anObserver
that observes the words and is notified when the words change. When there is a change, the observer'sonChange()
method is executed and updatesmWords
in theWordListAdapter
. - The data can be observed because it is
LiveData
. And what is observed is theLiveData<List<Word>>
that is returned by theWordViewModel
object. - The
WordViewModel
hides everything about the backend from the user interface. It provides methods for accessing the UI data, and it returnsLiveData
so thatMainActivity
can set up the observer relationship. Views, activities, and fragments only interact with the data through theViewModel
. 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 a 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 a
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 executeSELECT * from word_table ORDER BY word ASC
. - The result returned from the query is observed
LiveData
. Therefore, every time the data in Room changes, theObserver
interface'sonChanged()
method is executed and the UI is updated.
Related concept
The related concept documentation is Architecture Components.
Learn more
Android developer documentation:
- Guide to App Architecture
- Adding Components to your Project
- DAO
- Room DAOs
- Room package summary reference
- Handling Lifecycles with Lifecycle-Aware Components
LiveData
MutableLiveData
ViewModel
ViewModelProviders
- Defining data using Room entities
Blogs and articles:
- 7 Steps To Room (migrating an existing app)
- Understanding migrations with Room
- Lifecycle Aware Data Loading with Architecture Components
Codelabs:
- Android Persistence codelab (
LiveData
, Room, DAO) - Android lifecycle-aware components codelab (
ViewModel
,LiveData
,LifecycleOwner
,LifecycleRegistryOwner
) - Android Room with a View (same code as this practical)
Videos:
- Architecture Components overview
- Architecture Components: LiveData and Lifecycle
- Architecture Components: ViewModel
Code samples:
- Architecture Components code samples
- BasicSample (a not-so-basic but comprehensive sample)