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

Introduction

This codelab (practical) follows on from 10.1 Part A: Room, LiveData, and ViewModel. This codelab gives you more practice at using the API provided by the Room library to implement database functionality. You will add the ability to delete specific items from the database. This codelab also includes a coding challenge, in which you update the app so the user can edit existing data.

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 the following:

What you'll learn

What you'll do

You will extend the RoomWordsSample app that you created in the previous codelab. So far, that app displays a list of words, and users can add words. When the app closes and re-opens, the app re-initializes the database. Words that the user has added are lost.

In this practical, you extend the app so that it only initializes the data in the database if there is no existing data.

Then you add a menu item that allows the user to delete all the data.

You also enable the user to swipe a word to delete it from the database.

The RoomWordsSample app that you created in the previous practical deletes and re-creates the data whenever the user opens the app. This behavior isn't ideal, because users will want their added words to remain in the database when the app is closed. (Solution code for the previous practical is in GitHub.)

In this task you update the app so that when it opens, the initial data set is only added if the database has no data.

To detect whether the database contains data already, you can run a query to get one data item. If the query returns nothing, then the database is empty.

1.1 Add a method to the DAO to get a single word

Currently, the WordDao interface has a method for getting all the words, but not for getting any single word. The method to get a single word does not need to return LiveData, because your app will call the method explicitly when needed.

  1. In the WordDao interface, add a method to get any word:
 @Query("SELECT * from word_table LIMIT 1")
    Word[] getAnyWord();

Room issues the database query when the getAnyWord() method is called and returns an array containing one word. You don't need to write any additional code to implement it.

1.2 Update the initialization method to check whether data exists

Use the getAnyWord() method in the method that initializes the database. If there is any data, leave the data as it is. If there is no data, add the initial data set.

  1. PopulateDBAsync is an inner class in WordRoomDatbase. In PopulateDBAsync, update the doInBackground() method to check whether the database has any words before initializing the data:
@Override
protected Void doInBackground(final Void... params) {

       // If we have no words, then create the initial list of words
       if (mDao.getAnyWord().length < 1) {
           for (int i = 0; i <= words.length - 1; i++) {
               Word word = new Word(words[i]);
               mDao.insert(word);
           }
       }
   return null;
}
  1. Run your app and add several new words. Close the app and restart it. You should see the new words that you added, as the words should now persist when the app is closed and opened again.

In the previous practical, you used the deleteAll() method to clear out all the data when the database opened. The deleteAll() method was only invoked from the PopulateDbAsync class when the app started. You will now make the deleteAll() method available through the ViewModel so that your app can call the method whenever it's needed.

Here are the general steps for implementing a method to use the Room library to interact with the database:

2.1 Add deleteAll() to the WordDao interface and annotate it

  1. In WordDao, check that the deleteAll() method is defined and annotated with the SQL that runs when the method executes:
@Query("DELETE FROM word_table")
void deleteAll();

2.2 Add deleteAll() to the WordRepository class

Add the deleteAll() method to the WordRepository and implement an AsyncTask to delete all words in the background.

  1. In WordRepository, define deleteAllWordsAsyncTask as an inner class. Implement doInBackground() to delete all the words by calling deleteAll() on the DAO:
private static class deleteAllWordsAsyncTask extends AsyncTask<Void, Void, Void> {
   private WordDao mAsyncTaskDao;

   deleteAllWordsAsyncTask(WordDao dao) {
       mAsyncTaskDao = dao;
   }

   @Override
   protected Void doInBackground(Void... voids) {
       mAsyncTaskDao.deleteAll();
       return null;
   }
}
  1. In the WordRepository class, add the deleteAll() method to invoke the AsyncTask that you defined.
public void deleteAll()  {
   new deleteAllWordsAsyncTask(mWordDao).execute();
}

2.3 Add deleteAll() to the WordViewModel class

Make the deleteAll() method available to the MainActivity by adding it to the WordViewModel.

  1. In the WordViewModel class, add the deleteAll() method:
public void deleteAll() {mRepository.deleteAll();}

Next, you add a menu item to enable users to invoke deleteAll().

3.1 Add the Clear all data menu option

  1. In menu_main.xml, change the menu option title and id, as follows:
<item
   android:id="@+id/clear_data"
   android:orderInCategory="100"
   android:title="@string/clear_all_data"
   app:showAsAction="never" />
  1. In MainActivity, implement the onOptionsItemSelected() method to invoke the deleteAll() method on the WordViewModel object.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
   int id = item.getItemId();

   if (id == R.id.clear_data) {
       // Add a toast just for confirmation
       Toast.makeText(this, "Clearing the data...",
               Toast.LENGTH_SHORT).show();

       // Delete the existing data
       mWordViewModel.deleteAll();
       return true;
   }

   return super.onOptionsItemSelected(item);
}
  1. Run your app. In the Options menu, select Clear all data. All words should disappear.
  2. Restart the app. (Restart it from your device or the emulator; don't run it again from Android Studio) You should see the initial set of words.

Your app lets users add words and delete all words. In Tasks 4 and 5, you extend the app so that users can delete a word by swiping the item in the RecyclerView.

Again, here are the general steps to implement a method to use the Room library to interact with the database:

4.1 Add deleteWord() to the DAO and annotate it

  1. In WordDao, add the deleteWord() method:
@Delete
void deleteWord(Word word);


Because this operation deletes a single row, the @Delete annotation is all that is needed to delete the word from the database.

4.2 Add deleteWord() to the WordRepository class

  1. In WordRepository, define another AsyncTask called deleteWordAsyncTask as an inner class. Implement doInBackground() to delete a word by calling deleteWord() on the DAO:
private static class deleteWordAsyncTask extends AsyncTask<Word, Void, Void> {
   private WordDao mAsyncTaskDao;

   deleteWordAsyncTask(WordDao dao) {
       mAsyncTaskDao = dao;
   }

   @Override
   protected Void doInBackground(final Word... params) {
       mAsyncTaskDao.deleteWord(params[0]);
       return null;
   }
}
  1. In WordRepository, add the deleteWord() method to invoke the AsyncTask you defined.
public void deleteWord(Word word)  {
   new deleteWordAsyncTask(mWordDao).execute(word);
}

4.3 Add deleteWord() to the WordViewModel class

To make the deleteWord() method available to other classes in the app, in particular, MainActivity, add it to WordViewModel.

  1. In WordViewModel, add the deleteWord() method:
public void deleteWord(Word word) {mRepository.deleteWord(word);}

You have now implemented the logic to delete a word. As yet, there is no way to invoke the delete-word operation from the app's UI. You fix that next.

In this task, you add functionality to allow users to swipe an item in the RecyclerView to delete it.

Use the ItemTouchHelper class provided by the Android Support Library (version 7 and higher) to implement swipe functionality in your RecyclerView. The ItemTouchHelper class has the following methods:

5.1 Enable the adapter to detect the swiped word

  1. In WordListAdapter, add a method to get the word at a given position.
public Word getWordAtPosition (int position) {
   return mWords.get(position);
}
  1. In MainActivity, in onCreate(), create the ItemTouchHelper. Attach the ItemTouchHelper to the RecyclerView.
// Add the functionality to swipe items in the 
// recycler view to delete that item
ItemTouchHelper helper = new ItemTouchHelper(
    new ItemTouchHelper.SimpleCallback(0,
        ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
           @Override
           public boolean onMove(RecyclerView recyclerView,
                                 RecyclerView.ViewHolder viewHolder,
                                 RecyclerView.ViewHolder target) {
               return false;
           }

           @Override
           public void onSwiped(RecyclerView.ViewHolder viewHolder, 
                                int direction) {
               int position = viewHolder.getAdapterPosition();
               Word myWord = adapter.getWordAtPosition(position);
               Toast.makeText(MainActivity.this, "Deleting " + 
                       myWord.getWord(), Toast.LENGTH_LONG).show();

               // Delete the word
               mWordViewModel.deleteWord(myWord);
            }
       });

helper.attachToRecyclerView(recyclerView);

.

Things to notice in the code:

onSwiped() gets the position of the ViewHolder that was swiped:

int position = viewHolder.getAdapterPosition();

Given the position, you can get the word displayed by the ViewHolder by calling the getWordAtPosition() method that you defined in the adapter:

Word myWord = adapter.getWordAtPosition(position);

Delete the word by calling deleteWord() on the WordViewModel:

mWordViewModel.deleteWord(myWord);

Now run your app and delete some words

  1. Run your app. You should be able to delete words by swiping them left or right.

Android Studio project: RoomWordsWithDelete

Challenge: Update your app to allow users to edit a word by tapping the word and then saving their changes.

Hints

Make changes in NewWordActivity

You can add functionality to NewWordActivity, so that it can be used either to add a new word or edit an existing word.

Use an auto-generated key in Word

The Word entity class uses the word field as the database key. However, when you update a row in the database, the item being updated cannot be the primary key, because the primary key is unique to each row and never changes. So you need to add an auto-generated id to use as the primary key

@PrimaryKey(autoGenerate = true)
private int id;

@NonNull
@ColumnInfo(name = "word")
private String mWord;

Add a constructor for Word that takes an id

Add a constructor to the Word entity class that takes id and word as parameters. Make sure this additional constructor is annotated using @Ignore, because Room expects only one constructor by default in an entity class.

@Ignore
public Word(int id, @NonNull String word) {
   this.id = id;
   this.mWord = word;
}

To update an existing Word, create the Word using this constructor. Room will use the primary key (in this case the id) to find the existing entry in the database so it can be updated.

In WordDao, add the update() method like this:

@Update
void update(Word... word);

In WordRoomDatabase, increase the database version number, since database table schema is changed.

@Database(entities = {Word.class}, version = 2, exportSchema = false)

Challenge solution code

Android Studio project: RoomWordsWithUpdate

Writing database code

For example:

@Delete
void deleteWord(Word word);

@Update
void update(Word... word);

For example:

@Query("SELECT * from word_table ORDER BY word ASC")
LiveData<List<Word>> getAllWords();

@Query("DELETE FROM word_table")
void deleteAll();

ItemTouchHelper

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

Entities, data access objects (DAOs), and ViewModel:

ItemTouchHelper:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

You've learned several ways to store data. Choosing the right storage option depends on how large your data is, and how long the data needs to survive.

Create an app that demonstrates how data that is stored in at least two different locations survives configuration changes and the destruction of the app. You can do this by storing small pieces of data, such as strings, in different data stores.

Answer these questions

Question 1

Android Architecture Components provide some convenience annotations for DAOs. Which of the following are available? Select as many as apply.

Question 2

What are the benefits of using Architecture Components?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

To find the next practical codelab in the Android Developer Fundamentals (V2) course, see Codelabs for Android Developer Fundamentals (V2).

For an overview of the course, including links to the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).