Unit 6 Pathway 2 Activity 4: Persist Data with Room¶

Before you begin¶

  • Most production-quality apps have data that the app needs to persist. For example, the app might store a playlist of songs, items on a to-do list, records of expenses and income, a catalog of constellations, or a history of personal data. For such use cases, you use a database to store this persistent data.

  • Room is a persistence library that’s part of Android Jetpack. Room is an abstraction layer on top of a SQLite database. SQLite uses a specialized language (SQL) to perform database operations. Instead of using SQLite directly, Room simplifies the chores of database setup, configuration, and interactions with the app. Room also provides compile-time checks of SQLite statements.

  • An abstraction layer is a set of functions that hide the underlying implementation/complexity. It provides an interface to an existing set of functionality, like SQLite in this case.

  • The image below shows how Room, as a data source, fits in with the overall architecture recommended in this course. Room is a Data Source.

    ../_images/unit6-pathway2-activity4-section1-dcfae1d1e200f931_1440.png

Prerequisites¶

  • Ability to build a basic user interface (UI) for an Android app using Jetpack Compose.

  • Ability to use composables like Text, Icon, IconButton, and LazyColumn.

  • Ability to use the NavHost composable to define routes and screens in your app.

  • Ability to navigate between screens using a NavHostController.

  • Familiarity with the Android architecture component ViewModel. Ability to use ViewModelProvider.Factory to instantiate the ViewModels.

  • Familiarity with concurrency fundamentals.

  • Ability to use coroutines for long-running tasks.

  • Basic knowledge of SQLite databases and the SQL language.

What you’ll learn¶

  • How to create and interact with the SQLite database using the Room library.

  • How to create an entity, a data access object (DAO), and database classes.

  • How to use a DAO to map Kotlin functions to SQL queries.

What you’ll build¶

  • You’ll build an Inventory app that saves inventory items into the SQLite database.

What you need¶

  • Starter code for the Inventory app

  • A computer with Android Studio

  • Device or an emulator with API level 26 or higher

App overview¶

  • In this codelab, you work with a starter code of the Inventory app and add the database layer to it using the Room library. The final version of the app displays a list of items from the inventory database. The user has options to add a new item, update an existing item, and delete an item from the inventory database. For this codelab, you save the item data to the Room database. You complete the rest of the app’s functionality in the next codelab.

    Note

    The above screenshots are from the final version of the app at the end of the pathway, not the end of this codelab. These screenshots give you an idea of the final version of the app.

Starter app overview¶

Starter code overview¶

  • Open the project with the starter code in Android Studio.

  • Run the app on an Android device or an emulator. Make sure the emulator or connected device runs with an API level 26 or higher. Database Inspector works on emulators/devices that run API level 26 and higher.

    Note

    The Database Inspector lets you inspect, query, and modify your app’s databases while your app runs. The Database Inspector works with plain SQLite or with libraries built on top of SQLite, such as Room.

  • Notice that the app shows no inventory data.

  • Tap the floating action button (FAB) at the bottom right, which lets you add new items to the database.

  • The app navigates to a new screen where you can enter details for the new item.

Problems with the starter code¶

  • In the Add Item screen, enter an item’s details like name, price, and quantity of the Item.

  • Tap Save. The Add Item screen is not closed, but you can navigate back using the back key. The save functionality is not implemented, so the item details are not saved.

  • Notice that the app is incomplete and the Save button functionality is not implemented.

    ../_images/unit6-pathway2-activity4-section3-bae9fd572d154881_1440.png
  • In this codelab, you add the code that uses Room to save the inventory details in the SQLite database. You use the Room persistence library to interact with the SQLite database.

Code walkthrough¶

  • The starter code you downloaded has pre-designed screen layouts for you. In this pathway, you focus on implementing the database logic. The following section is a brief walkthrough of some of the files to get you started.

  • ui/home/HomeScreen.kt: this file is the home screen, or the first screen in the app, which contains the composables to display the inventory list. It has a FAB + to add new items to the list.

    ../_images/unit6-pathway2-activity4-section3-47cc655ae260796b_1440.png
  • ui/item/ItemEntryScreen.kt: this screen is similar to ItemEditScreen.kt. They both have text fields for the item details. This screen is displayed when the FAB is tapped in the home screen. The ItemEntryViewModel.kt is the corresponding ViewModel for this screen.

    ../_images/unit6-pathway2-activity4-section3-bae9fd572d154881_1440.png
  • ui/navigation/InventoryNavGraph.kt: this file is the navigation graph for the entire application.

Main components of Room¶

  • Kotlin provides an easy way to work with data through data classes. While it is easy to work with in-memory data using data classes, when it comes to persisting data, you need to convert this data into a format compatible with database storage. To do so, you need tables to store the data and queries to access and modify the data.

  • The following three components of Room make these workflows seamless.

    • Room entities represent tables in your app’s database. You use them to update the data stored in rows in tables and to create new rows for insertion.

    • Room Data Access Objects provide methods that your app uses to retrieve, update, insert, and delete data in the database.

    • Room Database class is the database class that provides your app with instances of the DAOs associated with that database.

  • You implement and learn more about these components later in the codelab. The following diagram demonstrates how the components of Room work together to interact with the database.

    ../_images/unit6-pathway2-activity4-section4-a3288e8f37250031_1440.png

Add Room dependencies¶

  • First, we need to add the required Room dependencies.

  • In build.gradle.kts (Module :app), in the dependencies block:

    //Room
    implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
    ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
    implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
    

Create an item Entity¶

  • An Entity class defines a table.

  • Each instance of this class (i.e. an object) represents a row in the table.

  • The entity class has mappings to tell Room how it intends to present and interact with the information in the database. In your app, the entity holds information about inventory items, such as item name, item price, and quantity of items available.

    ../_images/unit6-pathway2-activity4-section5-8c9f1659ee82ca43_1440.png
  • The @Entity annotation marks a class as a database Entity class. For each Entity class, the app creates a database table to hold the items. Each field of the Entity is represented as a column in the database, unless denoted otherwise.

  • Every entity instance (i.e. a row) stored in the table must have a primary key. The primary key is used to uniquely identify every record/entry in your database tables. After the app assigns a primary key, it cannot be modified; it represents the entity object as long as it exists in the database.

  • We’ll create an Entity class to store inventory information. In data/Item.kt, replace the contents with this code:

    package com.example.inventory.data
    
    import androidx.room.Entity
    import androidx.room.PrimaryKey
    
    /**
    * Entity data class represents a table (or a single row) in the database.
    */
    @Entity(tableName = "items")
    data class Item(
        // Primary key
        @PrimaryKey(autoGenerate = true)
        val id: Int = 0,
    
        // Item name
        val name: String,
    
        // Item price
        val price: Double,
    
        // Quantity in stock
        val quantity: Int
    )
    
    • @Entity(tableName = "items") tells Kotlin that this Entity defines an SQLite table named items

    • The code below tells Kotlin that the id field is the primary key, and makes Room automatically generate unique values for the primary key. The default value must be set to 0 to enable the auto-generation of primary key values.

      @PrimaryKey(autoGenerate = true)
      val id: Int = 0,
      
    • Great! Now that you have created an Entity class, you can create a Data Access Object (DAO) to access the database.

Create the item DAO¶

  • So far, we’ve created:

    • In data/Item.kt: a data class Item entity, that represents the Items table. Each instance of this class represents a row inside the Items table.

  • The Data Access Object (DAO) is a pattern you can use to separate the persistence layer from the rest of the application by providing an abstract interface. This isolation follows the single-responsibility principle.

  • The DAO hides all the complexities involved in performing database operations in the underlying persistence layer, separate from the rest of the application. This lets you change the data layer independently of the code that uses the data.

    ../_images/unit6-pathway2-activity4-section6-8b91b8bbd7256a63_1440.png
  • Next, we’ll define a DAO for Room. It will be a custom interface that provides convenience methods for querying/retrieving, inserting, deleting, and updating the database. Room generates an implementation of this class at compile time.

  • The Room library provides convenience annotations, such as @Insert, @Delete, and @Update, for defining methods that perform simple inserts, deletes, and updates without requiring you to write a SQL statement.

  • If you need to define more complex operations for insert, delete, update, or if you need to query the data in the database, use a @Query annotation instead.

  • For the Inventory app, you need the ability to do the following:

    • Insert or add a new item.

    • Update an existing item to update the name, price, and quantity.

    • Get a specific item based on its primary key, id

    • Get all items so you can display them.

    • Delete an entry in the database.

    ../_images/unit6-pathway2-activity4-section6-59aaa051e6a22e79_1440.png
  • To create the item DAO, create a new file, data/ItemDao.kt, with this code:

    package com.example.inventory.data
    
    import androidx.room.Dao
    import androidx.room.Delete
    import androidx.room.Insert
    import androidx.room.OnConflictStrategy
    import androidx.room.Query
    import androidx.room.Update
    import kotlinx.coroutines.flow.Flow
    
    @Dao
    interface ItemDao {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        suspend fun insert(item: Item)
    
        @Update
        suspend fun update(item: Item)
    
        @Delete
        suspend fun delete(item: Item)
    
        @Query("SELECT * from items WHERE id = :id")
        fun getItem(id: Int): Flow<Item>
    
        @Query("SELECT * from items ORDER BY name ASC")
        fun getAllItems(): Flow<List<Item>>
    }
    
    • Database operations can take a long time to execute, so they need to run on a separate thread. Room doesn’t allow database access on the main thread. That’s why insert(), update(), and delete() are suspend functions.

    • When using insert() to insert items into the database, conflicts can happen. For example, the code may erroneously add two entities with the same primary key. In the Inventory app, we only insert the entity from one place, that is the Add Item screen, so we are not expecting any conflicts, and can set the conflict strategy to Ignore, meaning Room will not insert a new item if that new item causes a conflict. That’s @Insert(onConflict = OnConflictStrategy.IGNORE).

    • Since this is just an interface, do we still need to write code to implement this interface? No. Room generates all the necessary code to insert the item into the database. When you call any of the DAO functions that are marked with Room annotations, Room executes the corresponding SQL query on the database. For example, when you call the above method, insert() from your Kotlin code, Room executes a SQL query to insert the entity into the database.

    • There is no convenience annotation for the remaining functionality, so @Query annotations are used to supply SQLite queries. Calling getItem(WTV_ID) will cause the SQLite query SELECT * from items WHERE id = WTV_ID to be run.

    • It is recommended to use Flow in the persistence layer. With Flow as the return type, you receive notification whenever the data in the database changes. Analogy: when following a content creator, you can turn on and receive notifications whenever they create a new post.

    • Room keeps this Flow updated for you, which means you only need to explicitly get the data once, e.g. by calling getItem() or getAllItems(), and the data will be updated whenever it changes in the database. Because of the Flow return type, Room also runs the query on the background thread. You don’t need to explicitly make it a suspend function and call it inside a coroutine scope.

      Note

      Flow in Room database can keep the data up-to-date by emitting a notification whenever the data in the database changes. This allows you to observe the data and update your UI accordingly.

Create a Database instance¶

  • So far, we’ve created:

    • In data/Item.kt: a data class Item entity, that represents the Items table. Each instance of this class represents a row inside the Items table.

    • In data/ItemDao.kt: an interface ItemDao, that contains methods to create, update, delete, and get items from the Items table. This is just an interface, the code for it will be provided by Room automatically.

  • In this task, we’ll create an InventoryDatabase class, that uses the data class Item and interface DAO. This class is responsible for creating the database, tables, and saving them to a file. This class is abstract, because Room will automatically generate most of the code needed, and we don’t have to code the implementation of this class ourselves.

  • Create a new file data/InventoryDatabase.kt.

  • Insert this code:

    package com.example.inventory.data
    
    import android.content.Context
    import androidx.room.Database
    import androidx.room.Room
    import androidx.room.RoomDatabase
    
    /**
    * Database class with a singleton Instance object.
    */
    @Database(entities = [Item::class], version = 1, exportSchema = false)
    abstract class InventoryDatabase : RoomDatabase() {
    
        // This abstract function returns an ItemDao, no need to code the function body, Room will automatically generate the code.
        abstract fun itemDao(): ItemDao
    
        companion object {
            @Volatile
            private var Instance: InventoryDatabase? = null
    
            fun getDatabase(context: Context): InventoryDatabase {
                // if the Instance is not null, return it, otherwise create a new database instance.
                return Instance ?: synchronized(this) {
                    Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
                        .fallbackToDestructiveMigration()
                        .build()
                        .also { Instance = it }
                }
            }
        }
    }
    
    • In this code:

      @Database(entities = [Item::class], version = 1, exportSchema = false)
      
      • entities = [Item::class] tells Room that the database contains only 1 entity (table), Item

      • Whenever the schema of the database table changes, version must be increased.

      • exportSchema = false disables schema version history backups. If set to true, Room will help to backup older versions of the schema into a folder.

    • The purpose of the companion object is to maintain one single instance of the database opened at a given time. A database object is computationally expensive to create and maintain, that’s why it’s preferred to only have one since instance.

    • @Volatile means that the value of that variable is never cached, and all reads and writes are to and from the main memory. These features help ensure the value of Instance is always up to date and is the same for all execution threads. It means that changes made by one thread to Instance are immediately visible to all other threads.

    • Multiple threads can potentially run getDatabase() concurrently, which results in two database instances instead of one. This issue is known as a race condition. Using synchronized {} ensures that only one thread can enter this block of code at any time, which makes sure the database only gets initialized once. Use synchronized {} to avoid race conditions.

    • Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database") creates the database inside a file named item_database. The context is the “environment” in which the database is created. InventoryDatabase::class.java is the name of the database class, InventoryDatabase, with an additional ::class.java appended.

    • fallbackToDestructiveMigration() causes the database to be destroyed and rebuilt whenever the schema changes, i.e. the entities (tables) changes. In this case, there is only one entity, Item. Example:

      • The first time the app is run, it uses the Item definition to create a database and a table for the Items. At this point, the Item class and the Items table are “in sync”, meaning they contain the same fields.

      • Some data is added to the Items table inside the database.

      • The Item class changes, causing the Item class and the Items table to now have different fields. All rows in the Items table are deleted.

      • The database is destroyed and rebuilt. The Item class and Items table are now “in sync” again.

    • fallbackToDestructiveMigration() is suitable for a sample app but in real life, other strategies are more appropriate. See Migration for more.

    • build() creates the database instance, and returns it.

    • also { Instance = it } sets Instance to the recently created database instance. It also returns the database instance that build() returned.

    Tip

    You can use this code as a template for your future projects. Replace the entities and DAOs specific to your app.

Implement the Repository¶

  • So far, we’ve created:

    • In data/Item.kt: a data class Item entity, that represents the Items table. Each instance of this class represents a row inside the Items table.

    • In data/ItemDao.kt: an interface ItemDao, that contains methods to create, update, delete, and get items from the Items table. This is just an interface, the code for it will be provided by Room automatically.

    • In data/InventoryDatabase.kt: a class InventoryDatabase, that “manages” the actual database. The actual database is stored inside a file. This class provides an ItemDao object, that can be used to create, update, delete rows inside the Items table.

  • Now we’ll create a class OfflineItemsRepository that provides an API for the app to call if the app needs to change some data. This class does not interact with the database directly, it needs to use an ItemDao object to help it do so.

  • Open data/ItemsRepository.kt, replace the contents with the code below.

    package com.example.inventory.data
    
    import kotlinx.coroutines.flow.Flow
    
    /**
    * Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
    */
    interface ItemsRepository {
        /**
        * Retrieve all the items from the the given data source.
        */
        fun getAllItemsStream(): Flow<List<Item>>
    
        /**
        * Retrieve an item from the given data source that matches with the [id].
        */
        fun getItemStream(id: Int): Flow<Item?>
    
        /**
        * Insert item in the data source
        */
        suspend fun insertItem(item: Item)
    
        /**
        * Delete item from the data source
        */
        suspend fun deleteItem(item: Item)
    
        /**
        * Update item in the data source
        */
        suspend fun updateItem(item: Item)
    }
    
  • This interface is very similar to interface ItemDao. What’s the diff?

    • interface ItemDao interacts directly with the SQLite database. It executes SQL queries.

    • interface ItemsRepository is a layer above interface ItemDao. It works at a higher level. It provides an API for the rest of the app to call, so that the app doesn’t need to call interface ItemDao methods directly.

  • To implement the ItemsRepository interface, open data/OfflineItemsRepository.kt, and replace the contents with this code:

    package com.example.inventory.data
    
    import kotlinx.coroutines.flow.Flow
    
    class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
        override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
    
        override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)
    
        override suspend fun insertItem(item: Item) = itemDao.insert(item)
    
        override suspend fun deleteItem(item: Item) = itemDao.delete(item)
    
        override suspend fun updateItem(item: Item) = itemDao.update(item)
    }
    
  • This code implements the ItemsRepository interface. It represents a repository that is offline, meaning no Internet connection is needed to use this repository.

  • Why do it like this?

    • If the app calls the interface ItemDao methods directly, then if one day the database is removed and replaced with a cloud data source instead, many parts of the app need to be changed.

    • By using interface ItemsRepository, if one day the database is removed and replaced with a cloud data source instead, then only the OfflineItemsRepository needs to be changed.

Implement AppContainer class¶

  • So far, we’ve created:

    • In data/Item.kt: a data class Item entity, that represents the Items table. Each instance of this class represents a row inside the Items table.

    • In data/ItemDao.kt: an interface ItemDao, that contains methods to create, update, delete, and get items from the Items table. This is just an interface, the code for it will be provided by Room automatically.

    • In data/InventoryDatabase.kt: a class InventoryDatabase, that “manages” the actual database. The actual database is stored inside a file. This class provides an ItemDao object, that can be used to create, update, delete rows inside the Items table.

    • In data/ItemsRepository.kt and data/OfflineItemsRepository.kt: a class OfflineItemsRepository that provides an API for the app to call if the app needs to change some data. This class does not interact with the database directly, it needs to use an ItemDao object to help it do so.

  • Now, we’ll instantiate the database, and pass in an ItemDao to the OfflineItemsRepository class.

  • In data/AppContainer.kt, replace the contents with this code:

    package com.example.inventory.data
    
    import android.content.Context
    
    /**
    * App container for Dependency injection.
    */
    interface AppContainer {
        val itemsRepository: ItemsRepository
    }
    
    /**
    * [AppContainer] implementation that provides instance of [OfflineItemsRepository]
    */
    class AppDataContainer(private val context: Context) : AppContainer {
        /**
        * Implementation for [ItemsRepository]
        */
        override val itemsRepository: ItemsRepository by lazy {
            OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
        }
    }
    
    • InventoryDatabase.getDatabase(context) instantiates the database instance. Next, the itemDao() creates an ItemDao, which is then passed to the OfflineItemsRepository. OfflineItemsRepository needs this ItemDao object to help it create, update, delete, and get rows from the Items table.

Add the save functionality¶

  • To save the app’s transient data and to also access the database, the ViewModels must be updated. The ViewModels use the DAO to interact with the database, and provide data to the UI. All database operations need to be run outside of the main UI thread, using coroutines and viewModelScope.

UI state class walkthrough¶

  • Open ui/item/ItemEntryViewModel.kt. The ItemUiState data class represents the UI state of an Item. The ItemDetails data class represents a single item.

  • The starter code provides three extension functions:

    • ItemDetails.toItem(): converts the ItemUiState UI state object to the Item entity type.

    • Item.toItemUiState(): converts the Item Room entity object to the ItemUiState UI state type.

    • Item.toItemDetails(): converts the Item Room entity object to the ItemDetails.

    // No need to copy, this is part of starter code
    /**
    * Represents Ui State for an Item.
    */
    data class ItemUiState(
        val itemDetails: ItemDetails = ItemDetails(),
        val isEntryValid: Boolean = false
    )
    
    data class ItemDetails(
        val id: Int = 0,
        val name: String = "",
        val price: String = "",
        val quantity: String = "",
    )
    
    /**
    * Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
    * not a valid [Double], then the price will be set to 0.0. Similarly if the value of
    * [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
    */
    fun ItemDetails.toItem(): Item = Item(
        id = id,
        name = name,
        price = price.toDoubleOrNull() ?: 0.0,
        quantity = quantity.toIntOrNull() ?: 0
    )
    
    fun Item.formatedPrice(): String {
        return NumberFormat.getCurrencyInstance().format(price)
    }
    
    /**
    * Extension function to convert [Item] to [ItemUiState]
    */
    fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
        itemDetails = this.toItemDetails(),
        isEntryValid = isEntryValid
    )
    
    /**
    * Extension function to convert [Item] to [ItemDetails]
    */
    fun Item.toItemDetails(): ItemDetails = ItemDetails(
        id = id,
        name = name,
        price = price.toString(),
        quantity = quantity.toString()
    )
    
  • These are used by the ViewModels to read and update the UI.

Update the ItemEntry ViewModel¶

  • In this task, you pass in the repository to the ItemEntryViewModel.kt file. You also save the item details entered in the Add Item screen into the database.

  • Open ItemEntryViewModel.kt.

  • Notice the validateInput() private function:

    // No need to copy over, this is part of starter code
    private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
        return with(uiState) {
            name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
        }
    }
    
  • The above function checks if the name, price, and quantity are empty. It’s used to verify user input before adding or updating the entity in the database.

  • Change the ItemEntryViewModel class to the below, but leave the code outside the class untouched.

    /**
    * ViewModel to validate and insert items in the Room database.
    */
    class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
        /**
        * Holds current item ui state
        */
        var itemUiState by mutableStateOf(ItemUiState())
            private set
    
        /**
        * Updates the [itemUiState] with the value provided in the argument. This method also triggers
        * a validation for input values.
        */
        fun updateUiState(itemDetails: ItemDetails) {
            itemUiState =
                ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
        }
    
        private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
            return with(uiState) {
                name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
            }
        }
    
        suspend fun saveItem() {
            if (validateInput()) {
                itemsRepository.insertItem(itemUiState.itemDetails.toItem())
            }
        }
    
    }
    
    • The constructor now takes in an ItemsRepository, which the ViewModel uses to save items into the database.

  • In ui/AppViewModelProvider.kt, modify the initializer {} for ItemEntryViewModel to pass in the repository as a parameter:

    object AppViewModelProvider {
        val Factory = viewModelFactory {
            // ...
    
            // Initializer for ItemEntryViewModel
            initializer {
                ItemEntryViewModel(inventoryApplication().container.itemsRepository)
            }
    
            // ...
        }
    }
    
  • The ItemEntryViewModel is now able to add entities to the database. Now, the UI must be updated the UI to use ItemEntryViewModel.

ItemEntryBody() composable walkthrough¶

  • In ui/item/ItemEntryScreen.kt file, the ItemEntryBody() composable is partially implemented in the starter code. Look at the ItemEntryBody() composable in the ItemEntryScreen() function call. Notice that the UI state and the updateUiState lambda are being passed as function parameters:

    // No need to copy over, part of the starter code
    ItemEntryBody(
        itemUiState = viewModel.itemUiState,            // UI state passed as parameter
        onItemValueChange = viewModel::updateUiState,   // updateUiState passed as parameter
        onSaveClick = { },
        modifier = Modifier
            .padding(
                start = innerPadding.calculateStartPadding(LocalLayoutDirection.current),
                end = innerPadding.calculateEndPadding(LocalLayoutDirection.current),
                top = innerPadding.calculateTopPadding()
            )
            .verticalScroll(rememberScrollState())
            .fillMaxWidth()
    )
    
  • Look at the ItemEntryBody() definition to see how the UI state is used. ItemEntryBody() displays an ItemInputForm, and a Save button. The ItemInputForm() displays the item details. The Save button is only enabled if text is entered in the text fields. itemUiState.isEntryValid is true if the text in all the text fields is valid (not empty)

    // No need to copy over, part of the starter code
    @Composable
    fun ItemEntryBody(
        itemUiState: ItemUiState,
        onItemValueChange: (ItemUiState) -> Unit,
        onSaveClick: () -> Unit,
        modifier: Modifier = Modifier
    ) {
        Column(
            // ...
        ) {
            ItemInputForm(
                itemDetails = itemUiState.itemDetails,   // UI state used to display item details
                onValueChange = onItemValueChange,
                modifier = Modifier.fillMaxWidth()
            )
            Button(
                onClick = onSaveClick,
                enabled = itemUiState.isEntryValid,   // UI state used to determine whether the Save Button is enabled or not
                shape = MaterialTheme.shapes.small,
                modifier = Modifier.fillMaxWidth()
            ) {
                Text(text = stringResource(R.string.save_action))
            }
        }
    }
    

Add a click handler to the Save button¶

  • Next, to tie everything together, we’ll add a click handler to the Save button. Inside the click handler, launch a coroutine and call saveItem() to save the data in the Room database.

  • In ItemEntryScreen.kt, inside the ItemEntryScreen composable function, add this code:

    val coroutineScope = rememberCoroutineScope()
    

    imports

    import androidx.compose.runtime.rememberCoroutineScope
    

    Note

    The rememberCoroutineScope() is a composable function that returns a CoroutineScope bound to the composition where it’s called. You can use the rememberCoroutineScope() composable function when you want to launch a coroutine outside of a composable, and ensure the coroutine is canceled after the scope leaves the composition. You can use this function when you need to control the lifecycle of coroutines manually, for example, to cancel an animation whenever a user event happens.

  • Inside ItemEntryBody(), add this code to launch a coroutine that saves the item and navigates back to the Inventory screen

    ItemEntryBody(
        itemUiState = viewModel.itemUiState,
        onItemValueChange = viewModel::updateUiState,
        onSaveClick = {
            coroutineScope.launch {
                viewModel.saveItem()
                navigateBack()
            }
        },
    
        // ...
    
  • Build and run your app.

  • Tap the + FAB.

  • In the Add Item screen, add the item details and tap Save. The app navigates back to the Inventory screen.

  • This action saves the data, but you cannot see the inventory data in the app. In the next task, you use the Database Inspector to view the data you saved.

    ../_images/unit6-pathway2-activity4-section9-63d24e14cf062873_1440.png

View the database content using Database Inspector¶

  • The Database Inspector lets you inspect, query, and modify your app’s databases while your app runs. This feature is especially useful for database debugging. The Database Inspector works with plain SQLite and with libraries built on top of SQLite, such as Room. Database Inspector works best on emulators/devices running API level 26 and higher.

    Note

    The Database Inspector only works with the SQLite library included in the Android operating system on API level 26 and higher. It doesn’t work with other SQLite libraries that you bundle with your app.

  • Select View âžś Tool Windows âžś App Inspection âžś Database Inspector.

  • Just above the words Database Inspector, make sure com.example.inventory appears somewhere. The item_database should appear in the Databases pane.

    ../_images/unit6-pathway2-activity4-section10-76408bd5e93c3432_1440.png
  • Expand the node for the item_database in the Databases pane and select Item to inspect.

  • Check the Live updates checkbox in the Database Inspector to automatically update the data it presents as you interact with your running app in the emulator or device.

    ../_images/unit6-pathway2-activity4-section10-9e21d9f7eb426008_1440.png
  • Congratulations! You created an app that can persist data using Room. In the next codelab, you will add a lazyColumn to your app to display the items on the database, and add new features to the app, like the ability to delete and update the entities. See you there!

Solution code¶

Summary¶

  • Define your tables as data classes annotated with @Entity. Define properties annotated with @ColumnInfo as columns in the tables.

  • Define a data access object (DAO) as an interface annotated with @Dao. The DAO maps Kotlin functions to database queries.

  • Use annotations to define @Insert, @Delete, and @Update functions.

  • Use the @Query annotation with an SQLite query string as a parameter for any other queries.

  • Use Database Inspector to view the data saved in the Android SQLite database.