Hard Prerequisites |
|
In this project, 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:
Tip: Being familiar with your starter app will make it easier to identify and fix problems, should you run into any.
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.
An entity represents an object or concept, and its properties, to store in the database. An entity class defines a table, and each instance of that class represents a row in the table. Each property defines a column. In your app, the entity is going to hold information about a night of sleep.
A query is a request for data or information from a database table or combination of tables, or a request to perform an action on the data. Common queries are for getting, inserting, and updating entities. For example, you could query for all the sleep nights on record, sorted by start time.
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.
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.
data class SleepNight(
var nightId: Long = 0L,
val startTimeMilli: Long = System.currentTimeMillis(),
var endTimeMilli: Long = startTimeMilli,
var sleepQuality: Int = -1
)
If prompted, import Entity and all other annotations from the androidx library.
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(...)
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,...
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
)
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:
In the database package, open SleepDatabaseDao.kt.
Notice that interface SleepDatabaseDao is annotated with @Dao. All DAOs need to be annotated with the @Dao keyword.
@Dao
interface SleepDatabaseDao {}
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)
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.
@Query
fun get(key: Long): SleepNight?
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.
Notice the :key. You use the colon notation in the query to reference arguments in the function.
("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
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()
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?
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
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:
Tip: The code will be much the same for any Room database, so you can use this code as a template.
In the database package, open SleepDatabase.kt.
In the file, create an abstract class called SleepDatabase that extends RoomDatabase.
Annotate the class with @Database.
@Database()
abstract class SleepDatabase : RoomDatabase() {}
entities = [SleepNight::class], version = 1, exportSchema = false
abstract val sleepDatabaseDao: SleepDatabaseDao
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 {}
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
fun getInstance(context: Context): SleepDatabase {}
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) {}
var instance = INSTANCE
return instance
if (instance == null) {}
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database")
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 project. A simple solution is to destroy and rebuild the database, which means that the data is lost.
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
@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
}
}
}
}
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.
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.
Here’s a quick run-through of the testing code, because it’s another piece of code that you can reuse:
When testing is done, the function annotated with @After executes to close the database.
Right-click on the test file in the Project pane and select Run ‘SleepDatabaseTest’.
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: