Abstracting SQL queries with Room Database in Android

While working with any kind of client app (be it mobile or desktop), you almost always need to have permanent storage in place that can allow you to store data, even when your application is stopped or closed by a user. Usually, such storage can be used to save the data from your app’s last use, or for saving data that was fetched from the internet so you don’t need to fetch it again, etc.

SQL in Android

In Android, we face the same situation multiple times in an app, and I faced this while working on AfterShoot, too. Every time I run the app, my machine learning model runs through all the images and to determine if a given image is good or not.

This process takes a lot of time (around 40 minutes for 9000 images), so I need to be able to store the results of the model somewhere so that the next time, I don’t have to run my model again, and I can simply fetch these results, which takes virtually no time.

To do this, we’ll be using an SQLite database for our app. If you’re experienced with Android development, you know that this is not only a very tedious task but it also involves a lot of boilerplate code in order to set up the database and ways to insert or get data to and from it.

For example, this is what a simple database that contains a table with the name of books looks like:

You’ll notice that it has hundreds of lines of code to accomplish something that seems very simple on its face.

Room, an alternative to traditional SQL

The folks working on the Android platform noticed this and introduced Room, which is a solution that allows developers to only define essential details about their database and automatically generate the code for the rest.

Room abstracts the boilerplate code that we had to write before so that we can focus our time and effort on improving the logic of the app instead of worrying about optimizing and fixing bugs in the database.

However, it still uses SQLite internally, so with all the flexibility that it provides, you don’t lose out on any features or performance that comes with using a traditional SQLiteOpenHelper class.

In this post, I’ll be outlining how I used Room to set up a database in my app in less than 50 lines of code.

Using Room in Android

Before using Room, it’s important that we take a step back and define what our database is going to look like; i.e. how many tables it’s going to have, what it looks like, how/what to access from the tables, etc.

For me, I have a single table with the following fields:

  • uri: Uri that uniquely identifies the image
  • name: Name of the image
  • dateTaken: The date on which the picture was taken
  • isBlurred: Is the image blurred (false by default)
  • processed: Is this image processed by my models (false by default)
  • size: The size of the image

You might have more tables depending on your needs. Once you’ve established what your database should look like, you can move on to the implementation steps.

Step 1: Adding the required dependencies

In order to use Room, we first need to add the following dependencies to our app’s build.gradle file:

dependencies {
    ...
    def room_version = "2.2.3"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    ...
}

Step 2: Define the Table(s) to be stored in your database

Up next, you need to define the table that needs to be stored in your database. To do this, create a Kotlin class for the object that needs saving and mark it with an @Entitiy annotation.

This is what it looks like:

@Entity(tableName = "afterShootImage")
data class Image(
        // Each member variable is a column, you can mark a column as your Primary key with @PrimaryKey
        @PrimaryKey
        val uri: Uri,
        val name: String,
        val size: String,
        val dateTaken: String?,
        var isBlurred: Boolean = false,
        var isOverExposed: Boolean = false,
        var isUnderExposed: Boolean = false,
        var isBlink: Boolean = false,
        var isCroppedFace: Boolean = false,
        var processed: Boolean = false)

As you can see, there’s no need to create a new class. Also, you can simply reuse your existing model by annotating it with @Entitiy.

ROOM also allows you to define an autoincrementing Primary Key:

@Entity(tableName = "afterShootImage")
data class Image(
        val uri: Uri,
        val name: String,
        val size: String,
        val dateTaken: String?,
        var isBlurred: Boolean = false,
        var isOverExposed: Boolean = false,
        var isUnderExposed: Boolean = false,
        var isBlink: Boolean = false,
        var isCroppedFace: Boolean = false,
        var processed: Boolean = false,
        // The id will automatically be incremented for every row and you needn't manage it yourself
        @PrimaryKey(autoGenerate = true)
        val id: Int = 0)

Step 3: Define what data you want to save or fetch to and from the database

Once we have the table in place, the next step is to define what data we want to access from the database, and how we want to do that. This includes all the CRUD methods (Create, Read, Update, Delete) and to define these methods, we have the DAO to help us.

DAO in Room stands for the Data Access Object, and we will be using an instance of DAO to query our database. This is how we define the DAO for our image table above:

@Dao
interface AfterShootDao {
    
    // query all images
    @Query("SELECT * FROM afterShootImage")
    fun getAllImages(): LiveData<List<Image>>

    @Query("SELECT * FROM afterShootImage WHERE isBlurred = 1")
    fun getBlurredImages(): LiveData<List<Image>>

    ...

}

As you can see, the DAO is an Interface, which means we don’t have to define any methods whatsoever. ROOM takes care of defining and generating these methods at compile time using the details we provided, along with the annotations. You can create more methods like deleting multiple images, inserting two images, etc.

It’s recommended that you create multiple DAOs for multiple Entity classes in your project.

Step 4: Defining the Master Database class and initializing it

Once we have the DAO and Entity configured, we then need to define a Database class that extends from RoomDatabase and instructs it to generate the methods defined in our DAO. This can be done as follows:

// define the entities and the database version (you need to increment the version whenever you make a change in the entity)
@Database(entities = [Image::class], version = 1)
abstract class AfterShootDatabase : RoomDatabase() {
    
    // define an abstract method, ROOM will generate the implementation for you 
    abstract fun getDao(): AfterShootDao
    
    // define more functions for all the Daos you have
    
}

Once we have defined the Database class, the next step is to initialize it and start using it to make queries. For the Database object, we should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and we rarely need access to multiple instances within a single process. We’ll do this in our AfterShootDatabase class itself, and here’s what it will look like:

@Database(entities = [Image::class], version = 1)
abstract class AfterShootDatabase : RoomDatabase() {

    abstract fun getDao(): AfterShootDao

    companion object {

        private var database: AfterShootDatabase? = null

        // Singleton database object, don't create this unnecessarily
        fun getDatabase(context: Context): AfterShootDatabase? {

            database ?: kotlin.run {
                // the builder needs a context, the Database class and a name for your database
                database = Room.databaseBuilder(context, AfterShootDatabase::class.java, "aftershootdb")
                        // destroy the earlier database if the version is incremented 
                        .fallbackToDestructiveMigration()
                        .build()
            }

            return database
        }
    }

}

While we’ve defined the default migration as destructive (as in, the old database is deleted whenever you make any changes in your database and increase the version), you can add custom migration queries as defined here:

Step 5: Use the database to run queries

The next step is to then use the Database object created to run queries. We’ll be running the following queries:

  1. Insert images in the database
  2. Fetch images from the database

This is what the code for inserting and querying images looks like:

private fun insetImages() {
        val images = listOf<Image>()
        // save all the images into the db
        CoroutineScope(Dispatchers.IO).launch {
            // get the dao and call insertMultipleImages() on it
            AfterShootDatabase.getDatabase(baseContext)?.getDao()?.insertMultipleImages(imageList)
        }
}

fun queryDatabase() { 
      CoroutineScope(Dispatchers.IO).launch {
            // get the dao and call getAllImages() on it
            val images = AfterShootDatabase.getDatabase(baseContext)?.getDao()?.getAllImages()
            // update the UI in the Main scope
            CoroutineScope(Dispatchers.Main).launch {
                val resultAdapter = ResultAdapter(images)
                rvImages.adapter = resultAdapter
            }
        }  
}

Upon running the app, you’ll probably run into the following error:

This error tells you that ROOM is unable to figure out a way to save the Uri column into the database, as they aren’t primitive data types. You might not get this error if you are only using primitive data types. But for complex data, we need to define a type convertor that tells Room how to convert the data to a primitive data type and vice versa. Let’s see how we can do that in the next and final step.

Step 6: Using a TypeConverter to save complex data into the database

We have to now define a TypeConvertor for the Uri column in our database. To do this, we need to create a new class and define 2 methods that define how to Convert the Uri to a String and vice versa

This is how it can be done:


class Converters {
    
    @TypeConverter
    fun stringToUri(uri: String): Uri = Uri.parse(uri)

    @TypeConverter
    fun uriToString(uri: Uri) = uri.toString()

}

Once done, the final step is to tell our master Database class to use this TypeConvertor for the entities:

@Database(entities = [Image::class], version = 1)
// define the TypeConverters to be used (for multiple, define them separated by a comma)
@TypeConverters(Converters::class)
abstract class AfterShootDatabase : RoomDatabase() {
    ...
}

And that’s it! ROOM also allows you to run complex SQL operations like Foreign keys, Joins, etc. in your database. While we haven’t covered it in this blog post, you can read up more on it here:

The complete source code covered in this blog post can be found here:

Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.

Have feedback? Let’s connect on Twitter.

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *