This codelab is part of the Android Kotlin Fundamentals course. You'll get the most value out of this course if you work through the codelabs in sequence. All the course codelabs are listed on the Android Kotlin Fundamentals codelabs landing page.

Introduction

Most apps have data that needs to be kept, even after the user closes the app. For example, the app might store a playlist, an inventory of game items, records of expenses and income, a catalog of constellations, or sleep data over time. Commonly, you would use a database to store persistent data.

Room is a database library that's part of Android Jetpack. Room takes care of many of the chores of setting up and configuring a database, and makes it possible for your app to interact with the database using ordinary function calls. Under the hood, Room is an abstraction layer on top of an SQLite database. Room's terminology, and the query syntax for more complex queries, follow the SQLite model.

The image below shows how the Room database fits in with the overall architecture recommended in this course.

What you should already know

You should be familiar with:

What you'll learn

What you'll do

In this codelab, you build the database portion of an app that tracks sleep quality. The app uses a database to store sleep data over time.

The app has two screens, represented by fragments, as shown in the figure below.

The first screen, shown on the left, has buttons to start and stop tracking. The screen shows all the user's sleep data. The Clear button permanently deletes all the data that the app has collected for the user.

The second screen, shown on the right, is for selecting a sleep-quality rating. In the app, the rating is represented numerically. For development purposes, the app shows both the face icons and their numerical equivalents.

The user's flow is as follows:

This app uses a simplified architecture, as shown below in the context of the full architecture. The app uses only the following components:

Step 1: Download and run the starter app

  1. Download the TrackMySleepQuality-Starter app from GitHub.
  2. Build and run the app. The app shows the UI for the SleepTrackerFragment fragment, but no data. The buttons do not respond to taps.

Step 2: Inspect the starter app

  1. Take a look at the Gradle files:
  1. Take a look at the packages and UI. The app is structured by functionality. The package contains placeholder files where you will add code throughout this series of codelabs.
  1. Take a look at the Util.kt file, which contains functions to help display sleep-quality data. Some code is commented out because it references a view model that you create later.
  2. Take a look at the androidTest folder (SleepDatabaseTest.kt). You will use this test to verify that the database works as intended.

In Android, data is represented in data classes, and the data is accessed and modified using function calls. However, in the database world, you need entities and queries.

Room does all the hard work for you to get from Kotlin data classes to entities that can be stored in SQLite tables, and from function declarations to SQL queries.

You must define each entity as an annotated data class, and the interactions as an annotated interface, a data access object (DAO). Room uses these annotated classes to create tables in the database, and queries that act on the database.

Step 1: Create the SleepNight entity

In this task, you define one night of sleep as an annotated data class.

For one night of sleep, you need to record the start time, end time, and a quality rating.

And you need an ID to uniquely identify the night.

  1. In the database package, find and open the SleepNight.kt file.
  2. Create the SleepNight data class with parameters for an ID, a start time (in milliseconds), an end time (in milliseconds), and a numerical sleep-quality rating.
data class SleepNight(
       var nightId: Long = 0L,
       val startTimeMilli: Long = System.currentTimeMillis(),
       var endTimeMilli: Long = startTimeMilli,
       var sleepQuality: Int = -1
)
  1. Before the class declaration, annotate the data class with @Entity. Name the table daily_sleep_quality_table. The argument for the tableName is optional, but recommended. You can look up other arguments in the documentation.

    If prompted, import Entity and all other annotations from the androidx library.
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(...)
  1. To identify the nightId as the primary key, annotate the nightId 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 night is unique.
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,...
  1. Annotate the remaining properties with @ColumnInfo. Customize the property names using parameters as shown below.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
       @PrimaryKey(autoGenerate = true)
       var nightId: Long = 0L,

       @ColumnInfo(name = "start_time_milli")
       val startTimeMilli: Long = System.currentTimeMillis(),

       @ColumnInfo(name = "end_time_milli")
       var endTimeMilli: Long = startTimeMilli,

       @ColumnInfo(name = "quality_rating")
       var sleepQuality: Int = -1
)
  1. Build and run your code to make sure it has no errors.

In this task, you define a data access object (DAO). On Android, the DAO provides convenience methods for inserting, deleting, and updating the database.

When you use a Room database, you query the database by defining and calling Kotlin functions in your code. These Kotlin functions map to SQL queries. You define those mappings in a DAO using annotations, and Room creates the necessary code.

Think of a DAO as defining a custom interface for accessing your database.

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 create your queries in Android Studio, the compiler checks your SQL queries for syntax errors.

For the sleep-tracker database of sleep nights, you need to be able to do the following:

Step 1: Create the SleepDatabase DAO

  1. In the database package, open SleepDatabaseDao.kt.
  2. Notice that interface SleepDatabaseDao is annotated with @Dao. All DAOs need to be annotated with the @Dao keyword.
@Dao
interface SleepDatabaseDao {}
  1. 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 SleepNight as its argument.

    That's it. Room will generate all the necessary code to insert the SleepNight into the database. When you call insert() from your Kotlin code, Room executes a SQL query to insert the entity into the database. (Note: You can call the function anything you want.)
@Insert
fun insert(night: SleepNight)
  1. Add an @Update annotation with an update() function for one SleepNight. The entity that's updated is the entity that has the same key as the one that's passed in. You can update some or all of the entity's other properties.
@Update
fun update(night: SleepNight)

There is no convenience annotation for the remaining functionality, so you have to use the @Query annotation and supply SQLite queries.

  1. Add a @Query annotation with a get() function that takes a Long key argument and returns a nullable SleepNight. You'll see an error for a missing parameter.
@Query
fun get(key: Long): SleepNight?
  1. The query is supplied as a string parameter to the annotation. Add a parameter to @Query. Make it a String that is a SQLite query.
("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
  1. Add another @Query with a clear() function and a SQLite query to DELETE everything from the daily_sleep_quality_table. This query does not delete the table itself.

    The @Delete annotation deletes one item, and you could use @Delete and supply a list of nights to delete. The drawback is that you need to fetch or know what's in the table. The @Delete annotation is great for deleting specific entries, but not efficient for clearing all entries from a table.
@Query("DELETE FROM daily_sleep_quality_table")
fun clear()
  1. Add a @Query with a getTonight() function. Make the SleepNight returned by getTonight() nullable, so that the function can handle the case where the table is empty. (The table is empty at the beginning, and after the data is cleared.)

    To get "tonight" from the database, write a SQLite query that returns the first element of a list of results ordered by nightId in descending order. Use LIMIT 1 to return only one element.
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?
  1. Add a @Query with a getAllNights() function:
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
  1. Though you won't see any visible changes, run your app to make sure it has no errors.

In this task, you create a Room database that uses the Entity and DAO that you created in the previous task.

You need to create an abstract database holder class, annotated with @Database. This class has one method that either creates an instance of the database if the database doesn't exist, or returns a reference to an existing database.

Getting a Room database is a bit involved, so here's the general process before you start with the code:

Step 1: Create the database

  1. In the database package, open SleepDatabase.kt.
  2. In the file, create an abstract class called SleepDatabase that extends RoomDatabase.

    Annotate the class with @Database.
@Database()
abstract class SleepDatabase : RoomDatabase() {}
  1. You'll see an error for missing entities and version parameters. The @Database annotation requires several arguments, so that Room can build the database.
entities = [SleepNight::class], version = 1, exportSchema = false
  1. The database needs to know about the DAO. Inside the body of the class, declare an abstract value that returns the SleepDatabaseDao. You can have multiple DAOs.
abstract val sleepDatabaseDao: SleepDatabaseDao
  1. Below that, define a companion object. The companion object allows clients to access the methods for creating or getting the database without instantiating the class. Since the only purpose of this class is to provide a database, there is no reason to ever instantiate it.
 companion object {}
  1. 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, once one has been created. This helps you avoid repeatedly opening connections to the database, which is expensive.

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 to all execution threads. It means that changes made by one thread to INSTANCE are visible to all other threads immediately, and you don't get a situation where, say, two threads each update the same entity in a cache, which would create a problem.

@Volatile
private var INSTANCE: SleepDatabase? = null
  1. Below INSTANCE, still inside the companion object, define a getInstance()method with a Context parameter that the database builder will need. Return a type SleepDatabase. You'll see an error because getInstance() isn't returning anything yet.
fun getInstance(context: Context): SleepDatabase {}
  1. Inside getInstance(), add a synchronized{} block. Pass in this so that you can access the context.

    Multiple threads can potentially ask for a database instance at the same time, resulting in two databases instead of one. This problem is not likely to happen in this sample app, but it's possible for a more complex app. Wrapping the code to get the database into synchronized 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.
synchronized(this) {}
  1. Inside the synchronized block, copy the current value of INSTANCE to a local variable instance. This is to take advantage of smart cast, which is only available to local variables.
var instance = INSTANCE
  1. Inside the synchronized block, return instance at the end of the synchronized block. Ignore the return type mismatch error; you won't ever return null once you are done.
return instance
  1. Above the return statement, add an if statement to check whether instance is null, that is, there is no database yet.
if (instance == null) {}
  1. If instance is null, use the database builder to get a database. In the body of the if statement, invoke Room.databaseBuilder and supply the context that you passed in, the database class, and a name for the database, sleep_history_database. To remove the error, you'll have to add a migration strategy and build() in the following steps.
instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database")
  1. 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()
  1. Finally, call .build().
.build()
  1. Assign INSTANCE = instance as the final step inside the if statement.
INSTANCE = instance
  1. Your final code should look like this:
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {

   abstract val sleepDatabaseDao: SleepDatabaseDao

   companion object {

       @Volatile
       private var INSTANCE: SleepDatabase? = null

       fun getInstance(context: Context): SleepDatabase {
           synchronized(this) {
               var instance = INSTANCE

               if (instance == null) {
                   instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database"
                   )
                           .fallbackToDestructiveMigration()
                           .build()
                   INSTANCE = instance
               }
               return instance
           }
       }
   }
}
  1. Build and run your code.

You now have all the building blocks for working with your Room database. This code compiles and runs, but you have no way of telling if it actually works. So, this is a good time to add some basic tests.

Step 2: Test the SleepDatabase

In this step, you run provided tests to verify that your database works. This helps ensure that the database works before you build onto it. The provided tests are basic. For a production app, you would exercise all of the functions and queries in all the DAOs.

The starter app contains an androidTest folder. This androidTest folder contains unit tests that involve Android instrumentation, which is a fancy way of saying that the tests need the Android framework, so you need to run the tests on a physical or virtual device. Of course, you can also create and run pure unit tests that do not involve the Android framework.

  1. In Android Studio, in the androidTest folder, open the SleepDatabaseTest file.
  2. To uncomment the code, select all the commented code and press the Cmd+/ or Control+/ keyboard shortcut.
  3. Take a look at the file.

Here's a quick run-through of the testing code, because it's another piece of code that you can reuse:

  1. Right-click on the test file in the Project pane and select Run 'SleepDatabaseTest'.
  2. After the tests run, verify in the SleepDatabaseTest pane that all the tests have passed.

Because all the tests passed, you now know several things:

Android Studio project: TrackMySleepQualityRoomAndTesting

When testing a database, you need to exercise all the methods defined in the DAO. To complete testing, add and execute tests to exercise the other DAO methods.

Udacity course:

Android Developer Documentation:

Other documentation and articles:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

How do you indicate that a class represents an entity to store in a Room database?

Question 2

The DAO (data access object) is an interface that Room uses to map Kotlin functions to database queries.

How do you indicate that an interface represents a DAO for a Room database?

Question 3

Which of the following statements are true about the Room database? Choose all that apply.

Question 4

Which of the following annotations can you use in your @Dao interface? Choose all that apply.

Question 5

How can you verify that your database is working? Select all that apply.

For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.