Android fundamentals 10.1 Part A: Room, LiveData, and ViewModel
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.
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 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.1.1'
archLifecycleVersion = '1.1.1'
}
Task 2: Create the Word entity
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.
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;}
}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;
Task 3: Create the DAO
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 an 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();
}
Task 4: Use LiveData
4.1 Return LiveData in WordDao
- In the
WordDao
interface, change thegetAllWords()
method signature so that the returnedList<Word>
is wrapped withLiveData<>
.
@Query("SELECT * from word_table ORDER BY word ASC")
LiveData<List<Word>> getAllWords();
Task 5: Add a Room database
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. Also set export schema tofalse
,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)
- 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.
// Create database here
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
WordRoomDatabase.class, "word_database")
.build();
- Add a migration strategy for the database.
- 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;
}
}
Task 6: Create the Repository
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;
}
}
Task 7: Create the ViewModel
7.1 Implement the WordViewModel
- Create a class called
WordViewModel
that extendsAndroidViewModel
.
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); }
}
Task 8: Add XML layouts for the UI
8.1 Add styles
- 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>
- 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: Create an Adapter and adding the RecyclerView
9.1 Create the WordListAdapter class
- Add a class
WordListAdapter
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);
}
}
}
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: Populate the database
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.
- 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: Connect the UI with the data
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);
- 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
.