Lab 9.3. Persist Data With Room
Lab 9.3. Persist Data With Room
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 setting up, configuring, and
interacting with the database. Room also provides compile-time checks of SQLite statements.
The image below shows how Room fits in with the overall architecture recommended in this
course.
Prerequisites
• You know how to build a basic user interface (UI) for an Android app.
• You know how to use activities, fragments, and views.
• You know how to navigate between fragments, using Safe Args to pass data between
fragments.
• You are familiar with the Android architecture components ViewModel, LiveData, and
Flow, and know how to use ViewModelProvider.Factory to instantiate the
ViewModels.
• You are familiar with concurrency fundamentals.
• You know how to use coroutines for long-running tasks.
• You have a basic understanding of SQL databases and the SQLite language.
2. App overview
In this codelab, you will work with a starter app called Inventory app, and add the database layer
to it using the Room library. The final version of the app displays a list items from the inventory
database using a RecyclerView. The user will have options to add a new item, update an
existing item, and delete an item from the inventory database (you'll complete the app's
functionality in the next codelab).
This codelab provides starter code for you to extend with features taught in this codelab. Starter
code may contain code that is familiar to you from previous codelabs, and also code that is
unfamiliar to you that you will learn about in later codelabs.
If you use the starter code from GitHub, note that the folder name is android-basics-kotlin-
inventory-app-starter. Select this folder when you open the project in Android Studio.
https://github.com/google-developer-training/android-basics-kotlin-inventory-app/tree/starter
To get the code for this codelab and open it in Android Studio, do the following.
3. In the dialog, click the Download ZIP button to save the project to your computer. Wait
for the download to complete.
4. Locate the file on your computer (likely in the Downloads folder).
5. Double-click the ZIP file to unpack it. This creates a new folder that contains the project
files.
3. In the Import Project dialog, navigate to where the unzipped project folder is located
(likely in your Downloads folder).
4. Double-click on that project folder.
5. Wait for Android Studio to open the project.
6. Click the Run button to build and run the app. Make sure it builds as expected.
7. Browse the project files in the Project tool window to see how the app is set-up.
Code walkthrough
The starter code you downloaded has the screen layouts pre-designed for you. In this pathway,
you will focus on implementing the database logic. Here is a brief walkthrough of some of the
files to get you started.
main_activity.xml
The main activity that hosts all the other fragments in the app. The onCreate() method retrieves
NavController from the NavHostFragment and sets up the action bar for use with the
NavController.
item_list_fragment.xml
The first screen shown in the app. It mainly contains a RecyclerView and a FAB. You will
implement the RecyclerView later in the pathway.
fragment_add_item.xml
This layout contains text fields for entering the details of the new inventory item to be added.
ItemListFragment.kt
This fragment contains mostly boilerplate code. In the onViewCreated() method, click listener
is set on FAB to navigate to the add item fragment.
AddItemFragment.kt
This fragment is used to add new items into the database. The onCreateView() function
initializes the binding variable and the onDestroyView() function hides the keyboard before
destroying the fragment.
• Data entities represent tables in your app's database. They are used to update the data
stored in rows in tables, and to create new rows for insertion.
• Data access objects (DAOs) provide methods that your app uses to retrieve, update,
insert, and delete data in the database.
• Database class holds the database and is the main access point for the underlying
connection to your app's database. The database class provides your app with instances of
the DAOs associated with that database.
You will implement and learn more about these components later in the codelab. The following
diagram demonstrates how the components of the Room work together to interact with the
database.
Add Room libraries
In this task, you'll add the required Room component libraries to your Gradle files.
// Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
Note: For the library dependencies in your gradle file, always use the most current stable release
version numbers from the AndroidX releases page.
5. Create an item Entity
Entity class defines a table, and each instance of this class represents a row in the database 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 is going to hold information about inventory
items such as item name, item price and stock available.
@Entity annotation marks a class as a database Entity class. For each Entity class a database
table is created to hold the items. Each field of the Entity is represented as a column in the
database, unless it is denoted otherwise (see Entity docs for details). Every entity instance that is
stored in the database must have a primary key. The primary key is used to uniquely identify
every record/entry in your database tables. Once assigned, the primary key cannot be modified, it
represents the entity object as long as it exists in the database.
In this task, you will create an Entity class. Define fields to store the following inventory
information for each item.
class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
Refresher on primary constructor: The primary constructor is part of the class header in a
Kotlin class: it goes after the class name (and optional type parameters).
Data classes
Data classes are primarily used to hold data in Kotlin. They are marked with the keyword data.
Kotlin data class objects have some extra benefits, the compiler automatically generates utilities
for comparing, printing and copying such as toString(), copy(), and equals().
Example:
To ensure consistency and meaningful behavior of the generated code, data classes have to fulfill
the following requirements:
Warning: The compiler only uses the properties defined inside the primary constructor for the
automatically generated functions. The properties declared inside the class body are excluded
from the generated implementations.
6. Above the Item class declaration, annotate the data class with @Entity. Use tableName
argument to give the item as the SQLite table name.
@Entity(tableName = "item")
data class Item(
...
)
Important: When prompted by Android Studio, import Entity and all other Room annotations
(which you will use later in the codelab) from the androidx library. For example,
androidx.room.Entity.
Note: @Entity annotation has several possible arguments. By default (no arguments to
@Entity), the table name will be the same as the class. The tableName argument let's you give a
different or a more helpful table name. This argument for the tableName is optional, but highly
recommended. For simplicity you will give the same name as the class name, that is item. There
are several other arguments for @Entity you can investigate in the documentation.
7. To identify the id as the primary key, annotate the id property with @PrimaryKey. Set
the parameter autoGenerate to true so that Room generates the ID for each entity. This
guarantees that the ID for each item is unique.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
...
)
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
The Data Access Object (DAO) is a pattern used to separate the persistence layer with the rest of
the application by providing an abstract interface. This isolation follows the single responsibility
principle, which you have seen in the previous codelabs.
The functionality of the DAO is to hide all the complexities involved in performing the database
operations in the underlying persistence layer from the rest of the application. This allows the
data access layer to be changed independently of the code that uses the data.
In this task, you define a Data Access Object (DAO) for the Room. Data access objects are the
main components of Room that are responsible for defining the interface that accesses the
database.
The DAO you will create will be a custom interface providing convenience methods for
querying/retrieving, inserting, deleting, and updating the database. Room will generate an
implementation of this class at compile time.
For common database operations, the Room library provides convenience annotations, such as
@Insert, @Delete, and @Update. For everything else, there is the @Query annotation. You can
write any query that's supported by SQLite.
As an added bonus, as you write your queries in Android Studio, the compiler checks your SQL
queries for syntax errors.
For the inventory app, you need to be able to do the following:
@Dao
interface ItemDao {
}
3. Inside the body of the interface, add an @Insert annotation. Below the @Insert, add an
insert() function that takes an instance of the Entity class item as its argument. The
database operations can take a long time to execute, so they should run on a separate
thread. Make the function a suspend function, so that this function can be called from a
coroutine.
@Insert
suspend fun insert(item: Item)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
Now the Room will generate all the necessary code to insert the item into the database. When you
call insert() from your Kotlin code, Room executes a SQL query to insert the entity into the
database. (Note: The function can be named anything you want; it doesn't have to be called
insert().)
5. Add an @Update annotation with an update() function for one item. The entity that's
updated has the same key as the entity that's passed in. You can update some or all of the
entity's other properties. Similar to the insert() method, make the following update()
method suspend.
@Update
suspend fun update(item: Item)
6. Add @Delete annotation with a delete() function to delete item(s). Make it a suspend
method. The @Delete annotation deletes one item, or a list of items. (Note: You need to
pass the entity(s) to be deleted, if you don't have the entity you may have to fetch it
before calling the delete() function.)
@Delete
suspend fun delete(item: Item)
There is no convenience annotation for the remaining functionality, so you have to use the
@Query annotation and supply SQLite queries.
7. Write a SQLite query to retrieve a particular item from the item table based on the given
id. You will then add Room annotation and use a modified version of the following
query in the later steps. In next steps, you will also change this into a DAO method using
Room.
8. Select all columns from the item
9. WHERE the id matches a specific value.
Example:
8. Change the above SQL query to use with the Room annotation and an argument. Add a
@Query annotation, supply the query as a string parameter to the @Query annotation. Add
a String parameter to @Query that is a SQLite query to retrieve an item from the item
table.
9. Select all columns from the item
10. WHERE the id matches the :id argument. Notice the :id. You use the colon notation in the
query to reference arguments in the function.
9. Below the @Query annotation add getItem() function that takes an Int argument and
returns a Flow<Item>.
Using Flow or LiveData as return type will ensure you get notified whenever the data in the
database changes. It is recommended to use Flow in the persistence layer. The Room keeps this
Flow updated for you, which means you only need to explicitly get the data once. This is helpful
to update the inventory list, which you will implement in the next codelab. 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 inside a coroutine scope.
11. Though you won't see any visible changes, run your app to make sure it has no errors.
7. Create a Database instance
In this task, you create a RoomDatabase that uses the Entity and DAO that you created in the
previous task. The database class defines the list of entities and data access objects. It is also the
main access point for the underlying connection.
The Database class provides your app with instances of the DAOs you've defined. In turn, the
app can use the DAOs to retrieve data from the database as instances of the associated data entity
objects. The app can also use the defined data entities to update rows from the corresponding
tables, or to create new rows for insertion.
You need to create an abstract RoomDatabase class, annotated with @Database. This class has
one method that either creates an instance of the RoomDatabase if it doesn't exist, or returns the
existing instance of the RoomDatabase.
• Create a public abstract class that extends RoomDatabase. The new abstract class you
defined acts as a database holder. The class you defined is abstract, because Room creates
the implementation for you.
• Annotate the class with @Database. In the arguments, list the entities for the database and
set the version number.
• Define an abstract method or property that returns an ItemDao Instance and the Room will
generate the implementation for you.
• You only need one instance of the RoomDatabase for the whole app, so make the
RoomDatabase a singleton.
• Use Room's Room.databaseBuilder to create your (item_database) database only if it
doesn't exist. Otherwise, return the existing database.
Tip: The following code can be used as a template for your future projects. The way you create
the RoomDatabase instance is similar to the process defined above. You may have to replace the
entities and Dao's specific to your app.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
3. The @Database annotation requires several arguments, so that Room can build the
database.
• Specify the Item as the only class with the list of entities.
• Set the version as 1. Whenever you change the schema of the database table, you'll have
to increase the version number.
• Set exportSchema to false, so as not to keep schema version history backups.
4. The database needs to know about the DAO. Inside the body of the class, declare an
abstract function that returns the ItemDao. You can have multiple DAOs.
5. Below the abstract function, define a companion object. The companion object allows
access to the methods for creating or getting the database using the class name as the
qualifier.
companion object {}
6. Inside the companion object, declare a private nullable variable INSTANCE for the
database and initialize it to null. The INSTANCE variable will keep a reference to the
database, when one has been created. This helps in maintaining a single instance of the
database opened at a given time, which is an expensive resource to create and maintain.
Annotate INSTANCE with @Volatile. The value of a volatile variable will never be cached, and
all writes and reads will be done to and from the main memory. This helps make sure the value
of INSTANCE is always up-to-date and the same for all execution threads. It means that changes
made by one thread to INSTANCE are visible to all other threads immediately.
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
8. Multiple threads can potentially run into a race condition and ask for a database instance
at the same time, resulting in two databases instead of one. Wrapping the code to get the
database inside a synchronized block means that only one thread of execution at a time
can enter this block of code, which makes sure the database only gets initialized once.
Inside getDatabase(), return INSTANCE variable or if INSTANCE is null, initialize it inside a
synchronized{} block. Use the elvis operator(?:) to do this. Pass in this the companion
object, that you want to be locked inside the function block. You will fix the error in the later
steps.
9. Inside the synchronized block, create a val instance variable, and use the database
builder to get the database. You will still have errors which you will fix in the next steps.
return instance
11. Inside the synchronized block, initialize the instance variable, and use the database
builder to get a database. Pass in the application context, the database class, and a name
for the database, item_database to the Room.databaseBuilder().
Android Studio will generate a Type Mismatch error. To remove this error, you'll have to add a
migration strategy and build() in the following steps.
Normally, you would have to provide a migration object with a migration strategy for when the
schema changes. A migration object is an object that defines how you take all rows with the old
schema and convert them to rows in the new schema, so that no data is lost. Migration is beyond
the scope of this codelab. A simple solution is to destroy and rebuild the database, which means
that the data is lost.
.fallbackToDestructiveMigration()
13. To create the database instance, call .build(). This should remove the Android Studio
errors.
.build()
INSTANCE = instance
15. At the end of the synchronized block, return instance. Your final code should look
like this:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
You now have all the building blocks for working with your Room. This code compiles and runs,
but you have no way of telling if it actually works. So, this is a good time to add a new item to
your Inventory database to test your database. To accomplish this, you need a ViewModel to talk
to the database.
8. Add a ViewModel
You have thus far created a database and the UI classes were part of the starter code. To save the
app's transient data and to also access the database, you need a ViewModel. Your Inventory
ViewModel will interact with the database via the DAO, and provide data to the UI. All database
operations will have to be run away from the main UI thread, you'll do that using coroutines and
viewModelScope.
Create Inventory ViewModel
1. In the com.example.inventory package, create a Kotlin class file
InventoryViewModel.kt.
2. Extend the InventoryViewModel class from the ViewModel class. Pass in the ItemDao
object as a parameter to the default constructor.
4. Click on the red bulb and select Implement Members, or you can override the create()
method inside the ViewModelProvider.Factory class as follows, which takes any class
type as an argument and returns a ViewModel object.
5. Implement the create() method. Check if the modelClass is the same as the
InventoryViewModel class and return an instance of it. Otherwise, throw an exception.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
Tip: The creation of the ViewModel factory is mostly boilerplate code, so you can reuse this
code for future ViewModel factories.
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
You need the name, price, and stock in hand for that particular item in order to add an entity to
the database. Later in the codelab, you will use the Add Item screen to get these details from the
user. In the current task, you use three strings as input to the ViewModel, convert them to an
Item entity instance, and save it to the database using the ItemDao instance. It's time to
implement.
2. To interact with the database off the main thread, start a coroutine and call the DAO
method within it. Inside the insertItem() method, use the viewModelScope.launch to
start a coroutine in the ViewModelScope. Inside the launch function, call the suspend
function insert() on itemDao passing in the item. The ViewModelScope is an
extension property to the ViewModel class that automatically cancels its child coroutines
when the ViewModel is destroyed.
3. In the InventoryViewModel class, add another private function that takes in three strings
and returns an Item instance.
4. Still inside the InventoryViewModel class, add a public function called addNewItem()
that takes in three strings for item details. Pass in item detail strings to
getNewItemEntry() function and assign the returned value to a val named newItem.
Make a call to insertItem() passing in the newItem to add the new entity to the
database. This will be called from the UI fragment to add Item details to the database.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
Notice that you did not use viewModelScope.launch for addNewItem(), but it is needed above
in insertItem() when you call a DAO method. The reason is that the suspend functions are
only allowed to be called from a coroutine or another suspend function. The function
itemDao.insert(item)is a suspend function.
You have added all the required functions to add entities to the database. In the next task you
will update the Add Item fragment to use the above functions.
9. Update AddItemFragment
1. In AddItemFragment.kt, at the beginning of the AddItemFragment class create a
private val called viewModel of the type InventoryViewModel. Use the by
activityViewModels() Kotlin property delegate to share the ViewModel across
fragments. You will fix the error in the next step.
2. Inside the lambda, call the InventoryViewModelFactory() constructor and pass in the
ItemDao instance. Use the database instance you created in one of the previous tasks to
call the itemDao constructor.
Tip: This is mostly boilerplate code, so you can reuse the code for future to create a ViewModel
instance using a ViewModel factory.
3. Below the viewModel definition, create a lateinit var called item of the type Item.
4. The Add Item screen contains three text fields to get the item details from the user. In
this step, you will add a function to verify if the text in the TextFields are not empty. You
will use this function to verify user input before adding or updating the entity in the
database. This validation needs to be done in the ViewModel and not in the Fragment. In
the InventoryViewModel class, add the following public function called
isEntryValid().
8. Inside the if block, call the addNewItem()method on the viewModel instance. Pass in
the item details entered by the user, use the binding instance to read them.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
9. Below the if block, create a val action to navigate back to the ItemListFragment.
Call findNavController().navigate(), passing in the action.
val action =
AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
Import androidx.navigation.fragment.findNavController.
11. To tie everything together, add a click handler to the Save button. In the
AddItemFragment class, above the onDestroyView() function, override the
onViewCreated() function.
12. Inside the onViewCreated() function, add a click handler to the save button, and call
addNewItem()from it.
13. Build and run your app. Tap the + Fab. In the Add Item screen, add the item details and
tap Save. This action saves the data, but you cannot see anything yet in the app. In the
next task, you will use the Database Inspector to view the data you saved.
View the database using Database Inspector
1. Run your app on an emulator or connected device running API level 26 or higher, if you
have not done so already. Database Inspector works best on emulator/devices running
API level 26.
2. In Android studio, select View > Tool Windows > Database Inspector from the menu
bar.
3. In the Database Inspector pane, select the com.example.inventory from the dropdown
menu.
4. The item_database in the Inventory app appears in the Databases pane. Expand the
node for the item_database and select Item to inspect. If your Databases pane is empty,
use your emulator to add some items to the database using the Add Item screen.
5. 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.
Congratulations! You have created an app that can persist the data using Room. In the next
codelab, you will add a RecyclerView to your app to display the items on the database and add
new features to the app like deleting and updating the entities. See you there!
Branch: room
To get the code for this codelab and open it in Android Studio, do the following.
3. In the dialog, click the Download ZIP button to save the project to your computer. Wait
for the download to complete.
4. Locate the file on your computer (likely in the Downloads folder).
5. Double-click the ZIP file to unpack it. This creates a new folder that contains the project
files.
Open the project in Android Studio
1. Start Android Studio.
2. In the Welcome to Android Studio window, click Open an existing Android Studio
project.
Note: If Android Studio is already open, instead, select the File > New > Import Project menu
option.
3. In the Import Project dialog, navigate to where the unzipped project folder is located
(likely in your Downloads folder).
4. Double-click on that project folder.
5. Wait for Android Studio to open the project.
6. Click the Run button to build and run the app. Make sure it builds as expected.
7. Browse the project files in the Project tool window to see how the app is set-up.
11. 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.
Blog posts
Videos
• Singleton pattern
• Companion objects
• SQLite Tutorial - An Easy Way to Master SQLite Fast