Read and update data with Room

1. Before you begin

You learned in the previous codelabs how to use a Room persistence library, an abstraction layer on top of a SQLite database, to store the app data. In this codelab, you'll add more features to the Inventory app and learn how to read, display, update, and delete data from the SQLite database using Room. You will use a LazyColumn to display the data from the database and automatically update the data when the underlying data in the database changes.

Prerequisites

  • Ability to create and interact with the SQLite database using the Room library.
  • Ability to create an entity, DAO, and database classes.
  • Ability to use a data access object (DAO) to map Kotlin functions to SQL queries.
  • Ability to display list items in a LazyColumn.
  • Completion of the previous codelab in this unit, Persist data with Room.

What you'll learn

  • How to read and display entities from a SQLite database.
  • How to update and delete entities from a SQLite database using the Room library.

What you'll build

  • An Inventory app that displays a list of inventory items and can update, edit, and delete items from the app database using Room.

What you'll need

  • A computer with Android Studio

2. Starter app overview

This codelab uses the Inventory app solution code from the previous codelab, Persist data with Room as the starter code. The starter app already saves data with the Room persistence library. The user can use the Add Item screen to add data to the app database.

Add Item screen with item details filled in.

Phone screen empty inventory

In this codelab, you extend the app to read and display the data, and update and delete entities on the database using a Room library.

Download the starter code for this codelab

To get started, download the starter code:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

Alternatively, you can download the repository as a zip file, unzip it, and open it in Android Studio.

If you want to see the starter code for this codelab, view it on GitHub.

3. Update UI state

In this task, you add a LazyColumn to the app to display the data stored in the database.

Phone screen with inventory items

HomeScreen composable function walkthrough

  • Open the ui/home/HomeScreen.kt file and look at the HomeScreen() composable.
@Composable
fun HomeScreen(
    navigateToItemEntry: () -> Unit,
    navigateToItemUpdate: (Int) -> Unit,
    modifier: Modifier = Modifier,
) {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()

    Scaffold(
        topBar = {
            // Top app with app title
        },
        floatingActionButton = {
            FloatingActionButton(
                // onClick details
            ) {
                Icon(
                    // Icon details
                )
            }
        },
    ) { innerPadding ->
     
       // Display List header and List of Items
        HomeBody(
            itemList = listOf(),  // Empty list is being passed in for itemList
            onItemClick = navigateToItemUpdate,
            modifier = modifier.padding(innerPadding)
                              .fillMaxSize()
        )
    }

This composable function displays the following items:

  • The top app bar with the app title
  • The floating action button (FAB) for the addition of new items to inventory 7b1535d90ee957fa.png
  • The HomeBody() composable function

The HomeBody()composable function displays inventory items based on the passed in list. As part of the starter code implementation, an empty list (listOf()) is passed to the HomeBody()composable function. To pass the inventory list to this composable, you must retrieve the inventory data from the repository and pass it into the HomeViewModel.

Emit UI state in the HomeViewModel

When you added methods to ItemDao to get items- getItem() and getAllItems()- you specified a Flow as the return type. Recall that a Flow represents a generic stream of data. By returning a Flow, you only need to explicitly call the methods from the DAO once for a given lifecycle. Room handles updates to the underlying data in an asynchronous manner.

Getting data from a flow is called collecting from a flow. When collecting from a flow in your UI layer, there are a few things to consider.

  • Lifecycle events like configuration changes, for example rotating the device, causes the activity to be recreated. This causes recomposition and collecting from your Flow all over again.
  • You want the values to be cached as state so that existing data isn't lost between lifecycle events.
  • Flows should be canceled if there's no observers left, such as after a composable's lifecycle ends.

The recommended way to expose a Flow from a ViewModel is with a StateFlow. Using a StateFlow allows the data to be saved and observed, regardless of the UI lifecycle. To convert a Flow to a StateFlow, you use the stateIn operator.

The stateIn operator has three parameters which are explained below:

  • scope - The viewModelScope defines the lifecycle of the StateFlow. When the viewModelScope is canceled, the StateFlow is also canceled.
  • started - The pipeline should only be active when the UI is visible. The SharingStarted.WhileSubscribed() is used to accomplish this. To configure a delay (in milliseconds) between the disappearance of the last subscriber and the stopping of the sharing coroutine, pass in the TIMEOUT_MILLIS to the SharingStarted.WhileSubscribed() method.
  • initialValue - Set the initial value of the state flow to HomeUiState().

Once you've converted your Flow into a StateFlow, you can collect it using the collectAsState() method, converting its data into State of the same type.

In this step, you'll retrieve all items in the Room database as a StateFlow observable API for UI state. When the Room Inventory data changes, the UI updates automatically.

  1. Open the ui/home/HomeViewModel.kt file, which contains a TIMEOUT_MILLIS constant and a HomeUiState data class with a list of items as a constructor parameter.
// No need to copy over, this code is part of starter code

class HomeViewModel : ViewModel() {

    companion object {
        private const val TIMEOUT_MILLIS = 5_000L
    }
}

data class HomeUiState(val itemList: List<Item> = listOf())
  1. Inside the HomeViewModel class, declare a val called homeUiState of the type StateFlow<HomeUiState>. You will resolve the initialization error shortly.
val homeUiState: StateFlow<HomeUiState>
  1. Call getAllItemsStream() on itemsRepository and assign it to homeUiState you just declared.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream()

You now get an error - Unresolved reference: itemsRepository. To resolve the Unresolved reference error, you need to pass in the ItemsRepository object to the HomeViewModel.

  1. Add a constructor parameter of the type ItemsRepository to the HomeViewModel class.
import com.example.inventory.data.ItemsRepository

class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
  1. In the ui/AppViewModelProvider.kt file, in the HomeViewModel initializer, pass the ItemsRepository object as shown.
initializer {
    HomeViewModel(inventoryApplication().container.itemsRepository)
}
  1. Go back to the HomeViewModel.kt file. Notice the type mismatch error. To resolve this, add a transformation map as shown below.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream().map { HomeUiState(it) }

Android Studio still shows you a type mismatch error. This error is because homeUiState is of the type StateFlow and getAllItemsStream() returns a Flow.

  1. Use the stateIn operator to convert the Flow into a StateFlow. The StateFlow is the observable API for UI state, which enables the UI to update itself.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream().map { HomeUiState(it) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
            initialValue = HomeUiState()
        )
  1. Build the app to make sure there are no errors in the code. There will not be any visual changes.

4. Display the Inventory data

In this task, you collect and update the UI state in the HomeScreen.

  1. In the HomeScreen.kt file, in the HomeScreen composable function, add a new function parameter of the type HomeViewModel and initialize it.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider


@Composable
fun HomeScreen(
    navigateToItemEntry: () -> Unit,
    navigateToItemUpdate: (Int) -> Unit,
    modifier: Modifier = Modifier,
    viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
  1. In the HomeScreen composable function, add a val called homeUiState to collect the UI state from the HomeViewModel. You use collectAsState(), which collects values from this StateFlow and represents its latest value via State.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

val homeUiState by viewModel.homeUiState.collectAsState()
  1. Update the HomeBody() function call and pass in homeUiState.itemList to the itemList parameter.
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier.padding(innerPadding)
)
  1. Run the app. Notice that the inventory list displays if you saved items in your app database. If the list is empty, add some inventory items to the app database.

Phone screen with inventory items

5. Test your database

Previous codelabs discuss the importance of testing your code. In this task, you add some unit tests to test your DAO queries, and then you add more tests as you progress through the codelab.

The recommended approach for testing your database implementation is writing a JUnit test that runs on an Android device. Because these tests don't require creating an activity, they are faster to execute than your UI tests.

  1. In the build.gradle.kts (Module :app) file, notice the following dependencies for Espresso and JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
  1. Switch to Project view and right-click on src > New > Directory to create a test source set for your tests.

9121189f4a0d2613.png

  1. Select androidTest/kotlin from the New Directory popup.

fba4ed57c7589f7f.png

  1. Create a Kotlin class called ItemDaoTest.kt.
  2. Annotate the ItemDaoTest class with @RunWith(AndroidJUnit4::class). Your class now looks something like the following example code:
package com.example.inventory

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
  1. Inside the class, add private var variables of the type ItemDao and InventoryDatabase.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao

private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
  1. Add a function to create the database and annotate it with @Before so that it can run before every test.
  2. Inside the method, initialize itemDao.
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before

@Before
fun createDb() {
    val context: Context = ApplicationProvider.getApplicationContext()
    // Using an in-memory database because the information stored here disappears when the
    // process is killed.
    inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
        // Allowing main thread queries, just for testing.
        .allowMainThreadQueries()
        .build()
    itemDao = inventoryDatabase.itemDao()
}

In this function, you use an in-memory database and do not persist it on the disk. To do so, you use the inMemoryDatabaseBuilder() function. You do this because the information need not be persisted, but rather, needs to be deleted when the process is killed.You are running the DAO queries in the main thread with .allowMainThreadQueries(), just for testing.

  1. Add another function to close the database. Annotate it with @After to close the database and run after every test.
import org.junit.After
import java.io.IOException

@After
@Throws(IOException::class)
fun closeDb() {
    inventoryDatabase.close()
}
  1. Declare items in the class ItemDaoTest for the database to use, as shown in the following code example:
import com.example.inventory.data.Item

private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
  1. Add utility functions to add one item, and then two items, to the database. Later, you use these functions in your test. Mark them as suspend so they can run in a coroutine.
private suspend fun addOneItemToDb() {
    itemDao.insert(item1)
}

private suspend fun addTwoItemsToDb() {
    itemDao.insert(item1)
    itemDao.insert(item2)
}
  1. Write a test for inserting a single item into the database, insert(). Name the test daoInsert_insertsItemIntoDB and annotate it with @Test.
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test


@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
    addOneItemToDb()
    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], item1)
}

In this test, you use the utility function addOneItemToDb()to add one item to the database. Then, you read the first item in the database. With assertEquals(), you compare the expected value with the actual value. You run the test in a new coroutine with runBlocking{}. This setup is the reason you mark the utility functions as suspend.

  1. Run the test and make sure it passes.

2f0ddde91781d6bd.png

8f66e03d03aac31a.png

  1. Write another test for getAllItems() from the database. Name the test daoGetAllItems_returnsAllItemsFromDB.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
    addTwoItemsToDb()
    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], item1)
    assertEquals(allItems[1], item2)
}

In the above test, you add two items to the database inside a coroutine. Then you read the two items and compare them with the expected values.

6. Display item details

In this task, you read and display the entity details on the Item Details screen. You use the item UI state, such as name, price, and quantity from the inventory app database and display them on the Item Details screen with the ItemDetailsScreen composable. The ItemDetailsScreen composable function is prewritten for you and contains three Text composables that display the item details.

ui/item/ItemDetailsScreen.kt

This screen is part of the starter code and displays the details of the items, which you see in a later codelab. You do not work on this screen in this codelab. The ItemDetailsViewModel.kt is the corresponding ViewModel for this screen.

de7761a894d1b2ab.png

  1. In the HomeScreen composable function, notice the HomeBody() function call. navigateToItemUpdate is being passed to the onItemClick parameter, which gets called when you click on any item in your list.
// No need to copy over 
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier
        .padding(innerPadding)
        .fillMaxSize()
)
  1. Open ui/navigation/InventoryNavGraph.kt and notice the navigateToItemUpdate parameter in the HomeScreen composable. This parameter specifies the destination for navigation as the item details screen.
// No need to copy over 
HomeScreen(
    navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
    navigateToItemUpdate = {
        navController.navigate("${ItemDetailsDestination.route}/${it}")
   }

This part of the onItemClick functionality is already implemented for you. When you click the list item, the app navigates to the item details screen.

  1. Click any item in the inventory list to see the item details screen with empty fields.

Item details screen with empty data

To fill the text fields with item details, you need to collect the UI state in ItemDetailsScreen().

  1. In UI/Item/ItemDetailsScreen.kt, add a new parameter to the ItemDetailsScreen composable of the type ItemDetailsViewModel and use the factory method to initialize it.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider

@Composable
fun ItemDetailsScreen(
    navigateToEditItem: (Int) -> Unit,
    navigateBack: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
  1. Inside the ItemDetailsScreen() composable, create a val called uiState to collect the UI state. Use collectAsState() to collect uiState StateFlow and represent its latest value via State. Android Studio displays an unresolved reference error.
import androidx.compose.runtime.collectAsState

val uiState = viewModel.uiState.collectAsState()
  1. To resolve the error, create a val called uiState of the type StateFlow<ItemDetailsUiState> in the ItemDetailsViewModel class.
  2. Retrieve the data from the item repository and map it to ItemDetailsUiState using the extension function toItemDetails(). The extension function Item.toItemDetails() is already written for you as part of the starter code.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

val uiState: StateFlow<ItemDetailsUiState> =
         itemsRepository.getItemStream(itemId)
             .filterNotNull()
             .map {
                 ItemDetailsUiState(itemDetails = it.toItemDetails())
             }.stateIn(
                 scope = viewModelScope,
                 started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
                 initialValue = ItemDetailsUiState()
             )
  1. Pass ItemsRepository into the ItemDetailsViewModel to resolve the Unresolved reference: itemsRepository error.
class ItemDetailsViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
    ) : ViewModel() {
  1. In ui/AppViewModelProvider.kt, update the initializer for ItemDetailsViewModel as shown in the following code snippet:
initializer {
    ItemDetailsViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Go back to the ItemDetailsScreen.kt and notice the error in the ItemDetailsScreen() composable is resolved.
  2. In the ItemDetailsScreen() composable, update the ItemDetailsBody() function call and pass in uiState.value to itemUiState argument.
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = {  },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Observe the implementations of ItemDetailsBody() and ItemInputForm(). You are passing the current selected item from ItemDetailsBody() to ItemDetails().
// No need to copy over

@Composable
private fun ItemDetailsBody(
    itemUiState: ItemUiState,
    onSellItem: () -> Unit,
    onDelete: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
       //...
    ) {
        var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
        ItemDetails(
             item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
         )

      //...
    }
  1. Run the app. When you click any list element on the Inventory screen, the Item Details screen displays.
  2. Notice that the screen is not blank anymore. It displays the entity details retrieved from the inventory database.

Item details screen with valid item details

  1. Tap the Sell button. Nothing happens!

In the next section, you implement the functionality of the Sell button.

7. Implement Item details screen

ui/item/ItemEditScreen.kt

The Item edit screen is already provided to you as part of the starter code.

This layout contains text field composables to edit the details of any new inventory item.

Edit item layout with item name item price and quantity in stock felids

The code for this app still isn't fully functional. For example, in the Item Details screen, when you tap the Sell button, the Quantity in Stock does not decrease. When you tap the Delete button, the app does prompt you with a confirmation dialog. However, when you select the Yes button, the app does not actually delete the item.

item delete confirmation pop up

Lastly, the FAB button aad0ce469e4a3a12.png opens an empty Edit Item screen.

Edit item screen with empty felids

In this section, you implement the functionalities of Sell, Delete and the FAB buttons.

8. Implement sell item

In this section, you extend the features of the app to implement the sell functionality. This update involves the following tasks:

  • Add a test for the DAO function to update an entity.
  • Add a function in the ItemDetailsViewModel to reduce the quantity and update the entity in the app database.
  • Disable the Sell button if the quantity is zero.
  1. In ItemDaoTest.kt, add a function called daoUpdateItems_updatesItemsInDB() with no parameters. Annotate with @Test and @Throws(Exception::class).
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
  1. Define the function and create a runBlocking block. Call addTwoItemsToDb() inside it.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
}
  1. Update the two entities with different values, calling itemDao.update.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
  1. Retrieve the entities with itemDao.getAllItems(). Compare them to the updated entity and assert.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
  1. Make sure the completed function looks like the following:
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
    itemDao.update(Item(1, "Apples", 15.0, 25))
    itemDao.update(Item(2, "Bananas", 5.0, 50))

    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
    assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
  1. Run the test and make sure it passes.

Add a function in the ViewModel

  1. In ItemDetailsViewModel.kt, inside the ItemDetailsViewModel class, add a function called reduceQuantityByOne() with no parameters.
fun reduceQuantityByOne() {
}
  1. Inside the function, start a coroutine with viewModelScope.launch{}.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope


viewModelScope.launch