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

1. Welcome

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:

Note: This course uses the terms “codelab” and “practical” interchangeably.

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:

  • Software architectural patterns that separate data from the UI.

Important: This practical implements the architecture defined in the Guide to App Architecture and explained in the Architecture Components concepts chapter, 10.1: Storing data with Room. It is highly recommended that you read the concepts chapter.

What you’ll learn

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

What you’ll do

  • Create an app with an Activity that displays words in a RecyclerView.

1.1 Create an app with one Activity

· Create an app named RoomWordsSample

· Uncheck Include Kotling support and Include C ++ support

· Select only for Phone & Tablet and the minimum set of API 14 SDK or higher

· Select the Basic Activity template

1.2 Update Gradle files

Add the code below to build.gradle in the Module: app file

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

Task 2. Create a data model for a single word

2.1. Create a data model for your word data

  1. Create a new class and call it WordItem.
  • private int mId; private String mWord;

Add an empty constructor.

Add getters and setters for the id and word.

Run your app. You will not see any visible UI changes, but there should be no errors

Task 3: Create the DA

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

Task 4: Use LiveData

4.1 Return LiveData in WordDao

Di dalam interface WOrdDao, ubah metod getAllWords() sehingga List<Word> yang dikembalikan dibungkus dengan 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, 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.

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();

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

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

Task 7: Create the ViewModel

7.1 Implement the WordViewModel

Create a class called WordViewModel that extends AndroidViewModel.

Warning:

  • Never pass context into ViewModel instances.
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); }

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

Add a style for text views in the values/styles.xml file:

8.2 Add item layout

  • Add a layout/recyclerview_item.xml layout:

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

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

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

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 the ViewModel, because all the activity's interactions are with the WordViewModel only.

private WordViewModel mWordViewModel;

In the onCreate() method, get a ViewModel from 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.

Task 12: Create an Activity for adding words

12.1 Create the NewWordActivity

Add these string resources in the values/strings.xml file:

Add a style for buttons in value/styles.xml:

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:

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:

12.2 Add code to insert a word into the database

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

Define the missing request code:

public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 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);

Run your app. When you add a word to the database in NewWordActivity, the UI automatically updates.