Did you ever consider the users of your app could have no internet connection? Android Room will store data locally enabling a smooth offline experience. In this post we will show how to store data in a Room database and abstract this mechanism away using the Repository pattern.

We start off with the example app of a previous post on Android RecyclerView. The complete code for this post can be found here on github.

We will only cover some of the aspects of storing data in a local database. More information can be found on the android developers page here.

Room Database

We begin defining the objects we want to save in the database. We use a normal Kotlin data class with android room annotations. We annotate the class with @Entity and specify the name of the database table.

@Entity(tableName = "actions",
        indices = [Index(value = ["code"], unique = true)])
data class DatabaseAction(
        @PrimaryKey(autoGenerate = true)
        var id: Long = 0L,
        var title: String,
        var code: String,
        var description: String,
        var type: String)

If you remember the domain object Action of the previous android post, you might notice that DatabaseAction is a copy except for the types and the id property. Not all types can be saved to the database so we need this representation.

Moreover we don’t want duplicate data in our database. Therefore we define an index on the action’s code. The database will now enforce that a DatabaseAction with an existing code can not be inserted.

Now we can define the methods to save and retrieve data from the actions table. Room has annotations that we can use to generate the implementations that we need. We collect them in the interface ActionDao annotated with @Dao.

@Dao
interface ActionDao {
    @Query("SELECT * FROM actions ORDER BY id DESC")
    fun fetchAllActions(): LiveData<List<DatabaseAction>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insertAll(actionList: List<DatabaseAction>)
}

Note that with @Query we can run any SQLite query that we would like. For this SELECT query we can choose return type List<DatabaseAction> or LiveData<List<DatabaseAction>>. The latter updates if the database entities change.

With the insert annotation we can say what to do when the insertion of a DatabaseAction fails. For example on the unique index on code we defined. Here we ignore the insert and move on to the next.

The only thing left to do is to define the database. We set the @Database annotation and specify alle the entity classes it entails. It also extends RoomDatabase and holds a reference to the ActionDao we just created.

@Database(entities = [DatabaseAction::class], version = 1, exportSchema = false)
abstract class GardenPlannerDatabase: RoomDatabase() {

    abstract val actionDao: ActionDao

    ...
}

Note that we did not talk about the database version. This is part of a whole different subject and will be out of scope for this post.

The Repository Pattern

We have everything in place to save and retrieve data from the database. But sometimes new is available from the ActionClient calling our (mocked backend). How can we provide a smooth experience and give our users the up to date data?

We use the repository pattern that appoints the database as the single source of truth and refreshes the database data with the data from the client. Below we see this pattern implemented.

class ActionRepository(private val database: GardenPlannerDatabase, private val actionClient: ActionClient) {

    val actionList: LiveData<List<Action>> = Transformations.map(database.actionDao.fetchAllActions()) {
        it.asDomainModel()
    }

    suspend fun refreshActions() {
        withContext(Dispatchers.IO) {
            val actionList = actionClient.fetchActions().await()
            database.actionDao.insertAll(
                                actionList.asDatabaseEntities())
        }
    }
}

Note that the repository has to map the database entities to domain entities. Also note that we did not cover the withContext(Dispatchers.IO) part for it is part of android coroutines and will be the subject of a later post.

Run the app and how the first time the data is displayed with a delay of 5 seconds. This simulates it being fetched from a brittle internet connection. The second time goes much faster for the repository gets it from the database where the data is stored. Even without internet the user can scroll through its data.

Hopefully you can are now able to use Android Room to save data locally in your own apps. Happy rooming!