A.I, Data and Software Engineering

Advanced android room persistent storage

A

Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Because Room takes care of several concerns for you, e.g. caching, networking, Android highly recommend using Room instead of SQLite. If you are coming from iOS, the good news is that Room is very similar to the core data in iOS development.

The task

Field nameType
uidInteger
nameString
photoBitmap
dobLocalDate
User table

We will create a user data model, similar to the one from Android documentation website. Nevertheless, we will add more complicated data to the model as well as some queries.

The user model will look like this.

Getting started

To use Room in your app, add the following dependencies to your app’s build.gradle file:KOTLINJAVA

First you will need to add kapt to your project build.gradle:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

Secondly, you will add the room’s dependencies.

dependencies {
  def room_version = "2.2.5"
  implementation "androidx.room:room-runtime:room_version"   kapt "androidx.room:room-compiler:room_version"
  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:room_version"   // optional - Test helpers   testImplementation "androidx.room:room-testing:room_version"
}

Sync your project before creating the data model.

room architecture
room architecture

Implement data model

With Room, we use all annotation, e.g. @Entity to denote the class as a data entity (table), @PrimaryKey for the primary key. The Room framework will handle the table creation for you.

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "name") val name: String?,
    @ColumnInfo(name = "dob") val dob: LocalDateTime?,
    @ColumnInfo(name = "photo") val photo: Bitmap? = null
)

data access objects (dao)

To access the table created above, we need to declare a DAO class. The following code provides the standard methods in the database, i.e. insert, delete, select. Similarly, we use @Query to define the custom query that we want to assign to the method, use @Delete to create standard delete user.

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>
    @Query("SELECT * FROM user WHERE name LIKE :name LIMIT 1")
    fun findByName(name: String): User
    @Insert
    fun insertAll(vararg users: User)
    @Delete
    fun delete(user: User)
}

Database

The database to store these tables can be defined as follow

@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Address the uncommon data types

Now if you try to compile your app, you will have the following error

error: Cannot figure out how to save this field into database.
Execution failed for task ':app:kaptDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
   > java.lang.reflect.InvocationTargetException (no error message)

It is because there are data types, e.g. Bitmap and LocalDate that Room does not know how to store it automatically. To solve this, we need to provide relevant methods for retrieve and save these data. We use @TypeConverter for these methods.

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): LocalDateTime? {
        return value?.let {
            Instant.ofEpochMilli(it)
                .atZone(ZoneId.of("UTC")).toLocalDateTime()
        }
    }
    @TypeConverter
    fun dateToTimestamp(date: LocalDateTime?): Long? {
        return date?.atOffset(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
    }
    @TypeConverter
    fun fromByteArray(bitmapdata: ByteArray?): Bitmap? {
        return bitmapdata?.let {
            BitmapFactory.decodeByteArray(bitmapdata, 0, bitmapdata.size)
        }
    }
    @TypeConverter
    fun dateToByteArray(bmp: Bitmap?): ByteArray? {
        val stream = ByteArrayOutputStream()
        bmp?.compress(Bitmap.CompressFormat.PNG, 100, stream)
        val byteArray: ByteArray = stream.toByteArray()
        return byteArray
    }
}

Now we can update the Appdatabase with the annotataion @TypeConverters as follow

@Database(entities = arrayOf(User::class), version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

It should compile normally.

Define relations

If you have another table, e.g. Library and you want to set user-library relations

@Entity
data class Library(
    @PrimaryKey val libraryId: Long,
    val userOwnerId: Long
)

To do this, create a new data class where each instance holds an instance of the parent entity and the corresponding instance of the child entity. Add the @Relation annotation to the instance of the child entity, with parentColumn set to the name of the primary key column of the parent entity and entityColumn set to the name of the column of the child entity that references the parent entity’s primary key.

data class UserAndLibrary(
    @Embedded val user: User,
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    val library: Library
)

Tips for Some other problems

  • If your project failed to build, it is likely that you have issue with Dao class. Check it thoroughly.
  • If you have problem with schema, you can add to defaultConfig of build.gradle
defaultConfig {
        //...
        kapt {
            arguments {
                arg("room.schemaLocation", "$projectDir/schemas")
            }
        }
        //...
    }

USEFUL LINKS

Add comment

💬

A.I, Data and Software Engineering

PetaMinds focuses on developing the coolest topics in data science, A.I, and programming, and make them so digestible for everyone to learn and create amazing applications in a short time.

Categories