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

dewi ayu paraswati
11 min readApr 22, 2019

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 be null.
  • 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 the tableName to "word_table".
  • 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;}
}
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 it WordDao.
  • 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 the deleteAll() method as follows:
@Query("DELETE FROM word_table")
  • Create a method called getAllWords() that returns a List of Words:
List<Word> getAllWords();
  • 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();
}

Task 4: Use LiveData

4.1 Return LiveData in WordDao

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

Task 5: Add a Room database

5.1 Implement a Room database

  • Create a public abstract class that extends RoomDatabase and call it WordRoomDatabase.
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 the entities class or classes creates corresponding tables in the database.) Set the version number. Also set export schema to false, exportSchemakeeps 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 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;
}
  • 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);
}
  • 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;
}
}

Task 7: Create the ViewModel

7.1 Implement the WordViewModel

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 the WordRepository.
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'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); }
}

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 the ConstraintLayout:
android:background="@color/colorScreenBackground"
  • In content_main.xml file, replace the TextViewelement 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"

Task 9: Create an Adapter and adding the RecyclerView

9.1 Create the WordListAdapter class

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

Here is the code :

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

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

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

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

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

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

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

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

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

9.2 Add RecyclerView to MainActivity

  • 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));
  • 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 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();
}
};
  • 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;
}
  • 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 the ViewModel, because all the activity's interactions are with the WordViewModel only.
private WordViewModel mWordViewModel;
  • In the onCreate() method, get a ViewModelfrom the ViewModelProviders class.
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
  • 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);
}
});
  • Run the app. The initial set of words appears in the RecyclerView.

--

--