Room DB: Advanced Data Persistence

This tutorial introduces more advanced concepts for use with the Room persistence library, such as migration and indexing. By Lance Gleason.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Testing Your Migrations

Now that you have the code needed to update your database, you’ll want to test your migration. To do that, you could add some code and Log statements to your activity. But a better way is to write an Espresso test.

To create an Espresso test, right-click on the (androidTest) version of your listmaster package. Select New ▸ Kotlin File/Class. Name it ListItemMigrationTest and press OK:

New Kotlin file

When the file opens, add in the following:

@RunWith(AndroidJUnit4::class)
class ListItemMigrationTest {

  private val TEST_DB_NAME = "migration_test"

  private lateinit var database: SupportSQLiteDatabase

  //1
  @Rule
  @JvmField
  val migrationTestHelperRule = MigrationTestHelper(
      InstrumentationRegistry.getInstrumentation(),
      "com.raywenderlich.listmaster.AppDatabase",
      FrameworkSQLiteOpenHelperFactory())

  //2
  @Before
  fun setup(){
    database = migrationTestHelperRule.createDatabase(TEST_DB_NAME, 1)
    database.execSQL("INSERT INTO list_categories (id, category_name) VALUES" +
        " (1, 'Purr Programming Supplies'), (2, 'Canine Coding Supplies')")
  }

  //3
  @After
  fun teardown(){
    database.execSQL("DROP TABLE IF EXISTS list_categories")
    database.execSQL("DROP TABLE IF EXISTS list_items")
    database.close()
  }
}

The code in the test class has the following capabilities:

  1. Creates a rule that initializes an Espresso test with the AppDatabase.
  2. Creates a version 1 instance of the database with data and runs the migration before every test.
  3. Removes all tables from the database after each test.

In the test setup, SQL statements are used to insert test data into version 1 of the database. This is because the new DAOs for version 2 are not available until all migrations have been executed.

As part of testing your migration, you need to get a version of the database that has already been migrated. To make your test more readable, paste the following helper method into your ListItemMigrationTest class.

private fun getMigratedRoomDatabase(): AppDatabase {
  //1
  val appDatabase = Room.databaseBuilder(
      InstrumentationRegistry.getTargetContext(),
      AppDatabase::class.java, TEST_DB_NAME)
      //2
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      //3
      .build()
  //4
  migrationTestHelperRule.closeWhenFinished(appDatabase)
  return appDatabase
}

Breaking down the parts of this method:

  1. Call the Room database builder, passing in the name of your test database.
  2. Add your migration to the builder.
  3. Build the database.
  4. Tell the test rule to close the database when finished.

LiveData Espresso Testing

When you are testing code that runs in another thread, a common problem is understanding how to get your test to wait for the threads to finish before doing an assert on the result.

In the case of LiveData, a query normally runs on a background thread, and you attach an observer to process the retrieved values. To get around this in tests, the projects includes a small Kotlin extension called blockingObserve in TestExtensions.kt. This file is under the root listmaster package in the (androidTest) section of the project.

Open it up and you will see the following:

fun <T> LiveData<T>.blockingObserve(): T? {
  var value: T? = null
  val latch = CountDownLatch(1)
  val innerObserver = Observer<T> {
    value = it
    latch.countDown()
  }
  observeForever(innerObserver)
  latch.await(2, TimeUnit.SECONDS)
  return value
}

It adds an observer to the LiveData object and blocks until a value is returned so that your test does not finish executing before the values are returned from the database.

Note: If this is your first time working with extensions, the Kotlin documentation is a great place to start for understanding how they work.

Now that you’ve built up the scaffolding of your test, it’s time to create a test to validate that the migration works. Paste the following method into ListItemMigrationTest:

@Test
fun migrating_from_1_to_2_retains_version_1_data() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  assertEquals(2, listCategories!!.size)
  assertEquals("Purr Programming Supplies",
      listCategories.first().categoryName)
  assertEquals(1, listCategories.first().id)
  assertEquals("Canine Coding Supplies",
      listCategories.last().categoryName)
  assertEquals(2, listCategories.last().id)
}

Reading the assertEquals statements, you will see verifications for the following things in the list_categories table using its DAO:

  • There are total of two records.
  • The first record is Purr Programming Supplies with an ID of 1.
  • The second record is Canine Coding Supplies with an ID of 2.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run:

Run the tests

You’ll need to select a device or emulator to run the Espresso tests on.

Verify that the result is “green” (passing):

Passing tests

Next, look in your assets directory and you will see a schema file for version 2 of your database called 2.json:
Schema version 2 file

Note: You should version these files in your project so that you can test your migrations as you increase the versions.

Now that you are in the testing groove, you are going to test inserting a record into the new table while referencing an existing category. To do this, paste the following method into your ListItemMigrationTest class:

@Test
fun inserting_a_record_into_list_items_after_migrating_from_1_to_2_succeeds() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  // insert a record in the new table
  val listItemDao = getMigratedRoomDatabase().listItemDao()
  val purrProgrammingListItem = ListItem("desk cushion", 1,
      listCategories!!.first().id)
  listItemDao.insertAll(purrProgrammingListItem)

  // validate that a record can be added to the new table
  val purrProgrammingList = listItemDao.getAll().blockingObserve()
  assertEquals(1, purrProgrammingList!!.size)
  val firstPurrProgrammingItem = purrProgrammingList.first()
  assertEquals("desk cushion", firstPurrProgrammingItem.itemDescription)
  assertEquals(1, firstPurrProgrammingItem.itemPriority)
  assertEquals(listCategories.first().id,
      firstPurrProgrammingItem.listCategoryId)
  assertEquals(1, firstPurrProgrammingItem.id)
}

This test does the following using the entity DAOs:

  • Inserts a record with a priority of 1 and item description of desk cushion associated with the Purr Programming Supplies category.
  • Checks that only one record was actually inserted.
  • Verifies that the persisted values match what we’ve added.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run. All tests should be green.

Migrating When Your App Initializes

Great! You’re confident that your migration code works. Now, you need to run this migration in the app. Since the goal is to have your app update the database, you’re going to need it to execute it when the user first opens the app. To do that, open the ListMasterApplication class and replace the onCreate() method with the following:

override fun onCreate() {
  super.onCreate()
  ListMasterApplication.database = Room.databaseBuilder(
      this,
      AppDatabase::class.java,
      "list-master-db")
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      .build()
}

The addMigrations() call does the following:

  • Checks if the migration has been applied to the database. If not, it runs the migration.
  • If migration has already been applied to the database, it will do nothing.

Wiring in the User Interface

Now that you have your ListItem hooked into the database, it’s time to wire it into your interface.

To start, open up the ListCategoryViewHolder class and add the following lines below the existing code inside the setListCategoryItem(listCategory: ListCategory) method:

holderListCategoryBinding.categoryName.rootView.setOnClickListener {
  val intent = Intent(listCategoriesActivity, ListItemsActivity::class.java)
  intent.putExtra(ListItemsActivity.LIST_CATEGORY_ID, listCategory.id)    
  intent.putExtra(ListItemsActivity.CATEGORY_NAME, listCategory.categoryName)
  listCategoriesActivity.startActivity(intent)
}

This adds an OnClickListener to each category in the list. The click listener launches a ListItemActivity, passing it the category ID and name.

Now, run the app by clicking on Run ▸ Run ‘app’ (you may need to switch the run configuration from the test class back to app first). Then, click on a category, such as Purr Programming Supplies, and you will see a screen that looks like this:

Category items screen

Your app is set up to use the Android Architecture Components MVVM pattern. When evaluating where to put the database query/insert, if you are not using MVVM, you might be inclined to put those queries in your activity. Part of the power of MVVM is the ability to put that logic into a component that is focused solely on data access.

In your case, you’re going to use two components:

  1. A repository object that focuses on interacting with your DAO and anything that is database-specific.
  2. A ViewModel that is lifecycle-aware by extending AndroidViewModel.

Create a ListItemRepository by right-clicking on the listitem package. Next, select New ▸ Kotlin File/Class. Name it ListItemRepository and press OK. Finally, paste in the following:

class ListItemRepository {
  //1
  private val listItemDao = ListMasterApplication.database!!.listItemDao()
  
  //2
  fun insertAll(vararg listItems: ListItem) {
    AsyncTask.execute {
      listItemDao.insertAll(*listItems)
    }
  }
} 

You are doing two things in this class:

  1. Getting a reference to your DAO.
  2. Providing a function to insert listItems in a background thread.

Next, you are going to create a lifecycle-managed ViewModel that will abstract the details of working with your repository. To do that, create a new Kotlin class in the listitem package by right-clicking the package name, selecting New and then Kotlin File/Class. Name it ListItemsViewModel and press OK. Finally, paste in the following:

//1
class ListItemsViewModel(application: Application) : AndroidViewModel(application) {

  //2
  private val listItemRepository: ListItemRepository = ListItemRepository()

  //3
  fun insertAll(vararg listItems: ListItem) {
    listItemRepository.insertAll(*listItems)
  }
}

This is doing a few things for you:

  1. Extends AndroidViewModel and takes a reference to the application in its constructor.
  2. Creates an instance of your repository and keeps a reference to it.
  3. Exposes an insertAll() method from your repository.

Note: If you are new to using View Models, check out this tutorial to learn about MVVM and how View Models fit into the pattern, and also our course MVVM on Android.

Now you are going to do some work in ListItemsActivity. To start, open it up and add a property for your ListItemsViewModel above onCreate():

private lateinit var listItemsViewModel: ListItemsViewModel

Inside onCreate(), add the following line right above the call to setupAddButton():

listItemsViewModel =
    ViewModelProviders.of(this).get(ListItemsViewModel::class.java)

This line of code initializes listItemsViewModel by calling ViewModelProviders to get an instance of your ListItemsViewModel.

Next, replace the setupAddButton() method with the following:

private fun setupAddButton() {
  activityListItemsBinding.fab.setOnClickListener {

    // Setup the dialog
    val alertDialogBuilder = AlertDialog.Builder(this).setTitle("Title")
    val dialogAddItemBinding = DialogAddItemBinding.inflate(layoutInflater)
    // 1
    val listItemViewModel = ListItemViewModel(ListItem("", 0, listCategory.id))
    dialogAddItemBinding.listItemViewModel = listItemViewModel

    alertDialogBuilder.setView(dialogAddItemBinding.root)

    /**
     * Setup the positive and negative buttons.
     * When the user clicks ok, a record is added to the db,
     * the db is queried and the RecyclerView is updated.
     */
    alertDialogBuilder.setPositiveButton(android.R.string.ok)
    { _: DialogInterface, _: Int ->
      // 2
      listItemsViewModel.insertAll(listItemViewModel.listItem)
    }
    alertDialogBuilder.setNegativeButton(android.R.string.cancel, null)
    alertDialogBuilder.show()
  }
}

This hooks up the + button by doing the following:

  1. Creates an instance of your ListItemViewModel and binds it to the dialog. Note that ListItemViewModel is different from ListItemsViewModel.
  2. Calls insertAll() on ListItemsViewModel when the user clicks OK.

Run the app by selecting the Run ▸ Run ‘app’ on the menu:

Run the app

If you click into a category, such as Purr Programming Supplies, you can then click the + button, add an item and priority, click OK, and it will take you back to the list that does not show anything. At this point, your first response might be:

Wheres the data

That’s because you have not hooked up your LiveData object to view these items, so time to fix that! Start by opening up ListItemRepository and add a new method named getAllByListCategoryId():

fun getAllByListCategoryId(listCategoryId: Long): LiveData<List<ListItem>> {
  return listItemDao.getAllByListCategoryId(listCategoryId)
}

This method takes a listCategoryId and returns a LiveData object from listItemDao. Next, open ListItemsViewModel and add a method also named getAllByListCategoryId() with different implementation:

fun getAllByListCategoryId(listCategoryId: Long): LiveData<List<ListItem>> {
  return listItemRepository.getAllByListCategoryId(listCategoryId)
}

This method takes same listCategoryId but returns a LiveData object from your listItemRepository.

Now, open up your ListItemsActivity and paste the following method:

private fun setupRecyclerAdapter() {
  val recyclerViewLinearLayoutManager = LinearLayoutManager(this)

  contentListItemsBinding = activityListItemsBinding.listItemsViewInclude!!
  contentListItemsBinding.listItemRecyclerView.layoutManager =
      recyclerViewLinearLayoutManager
  listItemAdapter = ListItemAdapter(listOf(), this)
  listItemsViewModel.getAllByListCategoryId(listCategory.id).observe(
      this, Observer { listItems: List<ListItem>? ->
    listItems?.let {
      listItemAdapter.itemList = it
      listItemAdapter.notifyDataSetChanged()
    }
  })
  contentListItemsBinding.listItemRecyclerView.adapter = listItemAdapter
}

This sets up your RecyclerView by placing an observer in your LiveData object returned by getAllByListCategoryId() to update the RecyclerView when new ListItem objects are added to a category.

Now, it’s time to add a call to your setupRecyclerAdapter() method at the end of onCreate:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  ...

  setupRecyclerAdapter()
}

It’s time to run the app again by clicking on the Run option and clicking on Run ‘app’. You should now see the item you added before.

When clicking into Purr Programming Supplies, you will see the Cat Nip you added before. Now, tap the + button, type in Cat Bed with a priority of 2, tap OK and you will see it in your list:

Items list