1. Before you begin
Most production quality apps have data that needs to be saved, even after the user closes the app. 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 most of these 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 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
, andFlow
, and know how to useViewModelProvider.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.
What you'll learn
- How to create and interact with the SQLite database using the Room library.
- How to create an entity, DAO, and database classes.
- How to use a data access object (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 installed.
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).
Below are screenshots from the final version of the app.
3. Starter app overview
Download the starter code for this 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.
To get the code for this codelab and open it in Android Studio, do the following.
Get the code
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- On the GitHub page for the project, click the Code button, which brings up a dialog.
- In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- 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.
- In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
- Browse the project files in the Project tool window to see how the app is set-up.
Starter code overview
- Open the project with the starter code in Android Studio.
- Run the app on an Android device, or on an emulator. Make sure the emulator or connected device is running API level 26 or higher. Database Inspector works best on emulator/devices running API level 26.
- The app shows no inventory data. Notice the FAB to add new items to the database.
- Click on the FAB. 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. Tap Save. The add item fragment is not closed. Navigate back using the system back key. The new item is not saved and is not listed on the inventory screen. Notice that the app is incomplete and the Save button functionality is not implemented.
In this codelab, you will add the database portion of an app that saves the inventory details in the SQLite database. You will be using the Room persistence library to interact with the SQLite database.
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.
4. Main components of Room
Kotlin provides an easy way to deal with data by introducing data classes. This data is accessed and possibly modified using function calls. However, in the database world, you need tables and queries to access and modify data. The following components of Room make these workflows seamless.
There are three major components in Room:
- 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.
- Open module level gradle file,
build.gradle (Module: InventoryApp.app)
In thedependencies
block, add the following dependencies for the Room library.
// Room implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version"
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.
- An
Int
to store the primary key. - A
String
to store the item name. - A
double
to store the item price. - An
Int
to store the quantity in stock.
- Open starter code in the Android Studio.
- Create a package called
data
undercom.example.inventory
base package.
- Inside the
data
package, create a Kotlin class calledItem
. This class will represent a database entity in your app. In the next step you will add corresponding fields to store inventory information. - Update the
Item
class definition with the following code. Declareid
of typeInt
,itemName
of typeString,
itemPrice
of typeDouble
, andquantityInStock
of typeInt
as parameters for the primary constructor. Assign a default value of0
toid
. This will be the primary key, an ID to uniquely identify every record/entry in yourItem
table.
class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
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:
// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}
To ensure consistency and meaningful behavior of the generated code, data classes have to fulfill the following requirements:
- The primary constructor needs to have at least one parameter.
- All primary constructor parameters need to be marked as
val
orvar
. - Data classes cannot be
abstract
,open
,sealed
orinner
.
To learn more about Data classes, check out the documentation.
- Convert the
Item
class to a data class by prefixing its class definition withdata
keyword.
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
- Above the
Item
class declaration, annotate the data class with@Entity
. UsetableName
argument to give theitem
as the SQLite table name.
@Entity(tableName = "item")
data class Item(
...
)
- To identify the
id
as the primary key, annotate theid
property with@PrimaryKey
. Set the parameterautoGenerate
totrue
so thatRoom
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,
...
)
- Annotate the remaining properties with
@ColumnInfo
. TheColumnInfo
annotation is used to customise the column associated with the particular field. For example, when using thename
argument, you can specify a different column name for the field rather than the variable name. Customize the property names using parameters as shown below. This approach is similar to usingtableName
to specify a different name to the database.
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
)
6. Create the item DAO
Data Access Object (DAO)
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:
- Insert or add a new item.
- Update an existing item to update 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.
Now, implement the item DAO in your app:
- In the
data
package, create Kotlin classItemDao.kt
. - Change the class definition to
interface
and annotate with@Dao
.
@Dao
interface ItemDao {
}
- Inside the body of the interface, add an
@Insert
annotation. Below the@Insert
, add aninsert()
function that takes an instance of theEntity
classitem
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)
- Add an argument
OnConflict
and assign it a value ofOnConflictStrategy.
IGNORE
. The argumentOnConflict
tells the Room what to do in case of a conflict. TheOnConflictStrategy.
IGNORE
strategy ignores a new item if it's primary key is already in the database. To know more about the available conflict strategies, check out the documentation.
@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()
.)
- Add an
@Update
annotation with anupdate()
function for oneitem
. 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 theinsert()
method, make the followingupdate()
methodsuspend
.
@Update
suspend fun update(item: Item)
- Add
@Delete
annotation with adelete()
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 thedelete()
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.
- 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. - Select all columns from the
item
WHERE
theid
matches a specific value.
Example:
SELECT * from item WHERE id = 1
- 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 aString
parameter to@Query
that is a SQLite query to retrieve an item from the item table. - Select all columns from the
item
WHERE
theid
matches the :id
argument. Notice the:id
. You use the colon notation in the query to reference arguments in the function.
@Query("SELECT * from item WHERE id = :id")
- Below the
@Query
annotation addgetItem()
function that takes anInt
argument and returns aFlow<Item>
.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): 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.
You may need to import Flow
from kotlinx.coroutines.flow.Flow
.
- Add a
@Query
with agetItems()
function: - Have the SQLite query return all columns from the
item
table, ordered in ascending order. - Have
getItems()
return a list ofItem
entities asFlow
.Room
keeps thisFlow
updated for you, which means you only need to explicitly get the data once.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- 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
.
Here's the general process for getting the RoomDatabase
instance:
- Create a
public abstract
class that extendsRoomDatabase
. The new abstract class you defined acts as a database holder. The class you defined is abstract, becauseRoom
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 theRoom
will generate the implementation for you. - You only need one instance of the
RoomDatabase
for the whole app, so make theRoomDatabase
a singleton. - Use
Room
'sRoom.databaseBuilder
to create your (item_database
) database only if it doesn't exist. Otherwise, return the existing database.
Create the Database
- In the
data
package, create a Kotlin classItemRoomDatabase.kt
. - In the
ItemRoomDatabase.kt
file, makeItemRoomDatabase
class as anabstract
class that extendsRoomDatabase
. Annotate the class with@Database
. You will fix the missing parameters error in the next step.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
- The
@Database
annotation requires several arguments, so thatRoom
can build the database.
- Specify the
Item
as the only class with the list ofentities
. - Set the
version
as1
. Whenever you change the schema of the database table, you'll have to increase the version number. - Set
exportSchema
tofalse
, so as not to keep schema version history backups.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 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.
abstract fun itemDao(): ItemDao
- 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 {}
- Inside the
companion
object, declare a private nullable variableINSTANCE
for the database and initialize it tonull
. TheINSTANCE
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
- Below
INSTANCE
, while still inside thecompanion
object, define agetDatabase()
method with aContext
parameter that the database builder will need. Return a typeItemRoomDatabase
. You'll see an error becausegetDatabase()
isn't returning anything yet.
fun getDatabase(context: Context): ItemRoomDatabase {}
- 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.
return INSTANCE ?: synchronized(this) { }
- 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.
val instance = Room.databaseBuilder()
- At the end of the
synchronized
block, returninstance
.
return instance
- Inside the
synchronized
block, initialize theinstance
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 theRoom.databaseBuilder()
.
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
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.
- Add the required migration strategy to the builder. Use
.fallbackToDestructiveMigration()
.
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()
- To create the database instance, call
.build()
. This should remove the Android Studio errors.
.build()
- Inside the
synchronized
block, assignINSTANCE = instance
.
INSTANCE = instance
- At the end of the
synchronized
block, returninstance
. Your final code should look like this:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
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
}
}
}
}
- Build your code to make sure there are no errors.
Implement Application class
In this task you will instantiate the database instance in the Application class.
- Open
InventoryApplication.kt
, create aval
calleddatabase
of the typeItemRoomDatabase
. Instantiate thedatabase
instance by callinggetDatabase()
onItemRoomDatabase
passing in the context. Uselazy
delegate so the instancedatabase
is lazily created when you first need/access the reference (rather than when the app starts). This will create the database (the physical database on the disk) on the first access.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
class InventoryApplication : Application(){
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
You will use this database
instance later in the codelab when creating a ViewModel instance.
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
- In the
com.example.inventory
package, create a Kotlin class fileInventoryViewModel.kt
. - Extend the
InventoryViewModel
class from theViewModel
class. Pass in theItemDao
object as a parameter to the default constructor.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- At the end of the
InventoryViewModel.kt
file outside the class, addInventoryViewModelFactory
class to instantiate theInventoryViewModel
instance. Pass in the same constructor parameter as theInventoryViewModel
that is theItemDao
instance. Extend the class from theViewModelProvider.Factory
class. You will fix the error regarding the unimplemented methods in the next step.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- Click on the red bulb and select Implement Members, or you can override the
create()
method inside theViewModelProvider.Factory
class as follows, which takes any class type as an argument and returns aViewModel
object.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
- Implement the
create()
method. Check if themodelClass
is the same as theInventoryViewModel
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")
Populate the ViewModel
In this task, you will populate the InventoryViewModel
class to add inventory data to the database. Observe the Item
entity and Add Item screen in the Inventory app.
@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.
- In the
InventoryViewModel
class, add aprivate
function calledinsertItem()
that takes in anItem
object and adds the data to the database in a non-blocking way.
private fun insertItem(item: Item) {
}
- To interact with the database off the main thread, start a coroutine and call the DAO method within it. Inside the
insertItem()
method, use theviewModelScope.launch
to start a coroutine in theViewModelScope
. Inside the launch function, call the suspend functioninsert()
onitemDao
passing in theitem
. TheViewModelScope
is an extension property to theViewModel
class that automatically cancels its child coroutines when theViewModel
is destroyed.
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
Import kotlinx.coroutines.launch,
androidx.lifecycle.
viewModelScope
com.example.inventory.data.Item
, if not automatically imported.
- In the
InventoryViewModel
class, add another private function that takes in three strings and returns anItem
instance.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- Still inside the
InventoryViewModel
class, add a public function calledaddNewItem()
that takes in three strings for item details. Pass in item detail strings togetNewItemEntry()
function and assign the returned value to a val namednewItem
. Make a call toinsertItem()
passing in thenewItem
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
- In
AddItemFragment.kt
, at the beginning of theAddItemFragment
class create aprivate val
calledviewModel
of the typeInventoryViewModel
. Use theby activityViewModels()
Kotlin property delegate to share theViewModel
across fragments. You will fix the error in the next step.
private val viewModel: InventoryViewModel by activityViewModels {
}
- Inside the lambda, call the
InventoryViewModelFactory()
constructor and pass in theItemDao
instance. Use thedatabase
instance you created in one of the previous tasks to call theitemDao
constructor.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
- Below the
viewModel
definition, create alateinit var
calleditem
of the typeItem
.
lateinit var item: Item
- 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 theInventoryViewModel
class, add the followingpublic
function calledisEntryValid()
.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
- In
AddItemFragment.kt
, below theonCreateView()
function create aprivate
function calledisEntryValid()
that returns aBoolean
. You will fix the missing return value error in the next step.
private fun isEntryValid(): Boolean {
}
- In the
AddItemFragment
class, implement theisEntryValid()
function. Call theisEntryValid()
function on theviewModel
instance, passing in the text from the text views. Return the value of theviewModel.isEntryValid()
function.
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
- In the
AddItemFragment
class below theisEntryValid()
function, add anotherprivate
function calledaddNewItem()
with no parameters and return nothing. Inside the function, callisEntryValid()
inside theif
condition.
private fun addNewItem() {
if (isEntryValid()) {
}
}
- Inside the
if
block, call theaddNewItem()
method on theviewModel
instance. Pass in the item details entered by the user, use thebinding
instance to read them.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
- Below the
if
block, create aval
action
to navigate back to theItemListFragment
. CallfindNavController
().navigate()
, passing in theaction
.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
Import androidx.navigation.fragment.findNavController.
- The complete method should look like the following.
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- To tie everything together, add a click handler to the Save button. In the
AddItemFragment
class, above theonDestroyView()
function, override theonViewCreated()
function. - Inside the
onViewCreated()
function, add a click handler to the save button, and calladdNewItem()
from it.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- 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
- 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.
- In Android studio, select View > Tool Windows > Database Inspector from the menu bar.
- In the Database Inspector pane, select the
com.example.inventory
from the dropdown menu. - 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.
- 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!
10. Solution code
The solution code for this codelab is in the GitHub repo and branch shown below.
To get the code for this codelab and open it in Android Studio, do the following.
Get the code
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- On the GitHub page for the project, click the Code button, which brings up a dialog.
- In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- 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.
- In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
- 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.
12. Learn more
Android Developer Documentation
- Save data in a local database using Room
- androidx.room
- Debug your database with the Database Inspector
Blog posts
Videos
Other documentation and articles