Implementing RoomDB in our App with MVVM & DI in Android: (Day 04)

In our last blog post, we tried to set up a basic app with dependency injection that we planned to use in our future blog posts to create a login, and signup flow, which will help us understand how DI (dependency injection) and MVVM (Model View View-Model) architecture can work seamlessly to create a highly scalable app.

The Project

The code to this blog post can be found here.

As already mentioned, the project we are trying to create is a simple login and signup workflow. Now, from login and signup workflow, we know that we'll be needing the following features.

  1. We need a database to store the signup data of a new user.

  2. We need methods to fetch this data back when a user tries to log in to verify if the login ID and password are correct or not

  3. We need also to store all of this data on a remote server, so that we are able to fetch these user data details even if the local database is cleared.

Now if we go back to our MVVM architecture and try to understand where all of these elements fit, it should look something like this.

Approach

Since the elements higher in the hierarchy depend upon the elements that are lower in comparison to them, it would make sense to create these classes and components one by one from the bottom, so that we always have the required dependency when needed by the project.

Please note that this is not the standard method of development, but we'll follow it anyway because it'll help us understand how these components fit together.

In our today's post, we'll only try to implement one of the components of our project which is the RoomDB.

What is RoomDB?

RoomDB simply put is a persistence library providing an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite. The library helps to ensure a clean and simple-to-use API to your database, facilitating the process of data caching in your Android application. RoomDB performs compile-time verification of SQL queries, ensuring that they are correctly structured before execution, thereby reducing runtime errors. It also supports Coroutines and RxJava for data transactions, making it a modern and reactive solution for data management in Android development.

This is just a simple explanation of what RoomDB is, we'll try to understand it further as we move ahead in our series.

Let's start coding.

Step 1

Let's start by adding the necessary dependencies to the app.gradle file to integrate RoomDB into our project

dependencies {
    //Other dependencies
    def dagger_version = '2.46.1'
    implementation "com.google.dagger:dagger:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
    //dagger android
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"

    // Room components
    implementation "androidx.room:room-runtime:2.5.2"
    kapt "androidx.room:room-compiler:2.5.2"
    implementation "androidx.room:room-ktx:2.5.2" }

Step 2

Create a new package named db, this package will hold all our classes required for the RoomDB setup.

Inside this package, add a new class named AppDatabase as follows.

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

Let's try to understand this code

Explanation

  1. @Database Annotation:

    • entities = []: This parameter specifies the entities (or tables) in the database. Here, it indicates that the database contains no table represented by the empty array entities.

    • version = 1: This parameter indicates the version of the database. It is used for managing database migrations, which are necessary when you change the database schema in a new app version.

    • exportSchema = false: This parameter, when set to false, prevents Room from exporting the database schema into a JSON file. Keeping the schema information can be useful for version control and migration purposes, but it is not mandatory.

  2. AppDatabase Class:

    • abstract class AppDatabase: This defines an abstract class named AppDatabase. Being abstract means that this class cannot be instantiated directly; it serves as a blueprint for the Room to implement the database.

    • : RoomDatabase(): This indicates that AppDatabase extends the RoomDatabase class, which is a part of the Room Persistence Library. This class contains the database holder and serves as the main access point for the underlying connection to your app's persisted data.

Step 3

But right now, we do not have any tables in this Database. So let's go ahead and create a UserData class that will serve as a table entity as shown below.

@Entity(tableName = "UserData")
class UserData {
    @PrimaryKey(autoGenerate = false)

    var uid: String = ""

    var name: String? = null

    var email: String? = null

    var password: String? = null
}

Explanation

  • @Entity(tableName = "UserData"): This annotation marks the UserData class as an entity. The tableName parameter is used to specify the name of the table as "UserData". If tableName is not specified, the name of the class will be used as the table name by default.

  • @PrimaryKey(autoGenerate = false): This annotation is used to denote that the uid property is the primary key for the UserData table. The autoGenerate parameter is set to false, meaning that the ID values will not be generated automatically; you will need to assign them manually. If you set it to true, Room would automatically generate unique IDs for each entry.

  • The rest of the fields will work as columns of the table.

Step 4

Now that we have a table created, let's add it to our database entities list as follows.

@Database(entities = [UserData::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
}

Okay, now that we have our table added to our database, we need a way to execute queries on our tables & database.

In RoomDB, this is done with the help of DAOs or Data Access Objects.

DAO (Data Access Object)

DAOs are a part of the Room persistence library that provides an abstraction layer for data operations on your database. It is an interface that contains methods that offer a way to fetch and store data in a database in an object-oriented fashion. Each method in a DAO corresponds to a database operation, such as inserting, updating, or deleting data.

Let's create a simple DAO class ourselves that will store some data into our newly created table.

@Dao
interface UserDAO{
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(userData: UserData)
}

Explanation

  1. @Dao Annotation:

    • @Dao: This annotation identifies the interface as a DAO class for Room. Room will generate code for the methods defined in this interface, providing the necessary SQL operations to implement them.
  2. UserDAO Interface:

    • interface UserDAO: This defines an interface named UserDAO. Interfaces in Kotlin can contain declarations of abstract methods, as well as method implementations. Here, it serves as a contract for the DAO, listing the database operations that can be performed.
  3. @Insert Annotation:

    • @Insert(onConflict = OnConflictStrategy.REPLACE): This annotation identifies the method as an insert operation for Room. The onConflict parameter defines how conflicts should be resolved when inserting data. Here, it is set to OnConflictStrategy.REPLACE, which means that if there is a conflict (i.e., a row with the same primary key already exists), the existing row will be replaced with the new data. Other strategies include ABORT, IGNORE, etc.

Now we need to provide this DAO to our database as follows.

@Database(entities = [UserData::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDAO(): UserDAO
}

Injecting RoomDB into our Dagger Graph

Now that we have our basic RoomDB setup, we need to figure out a way to provide this as a dependency to the rest of our components i.e. Repository, but since we do not have a repository class yet, we'll test our RoomDB implementation by injecting it in MainActivity.

Let's start by creating a simple DatabaseModule in the modules package inside di module, and use it to provide database and DAOs as follows.

@Module
class DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(application: Application): AppDatabase {
        return Room.databaseBuilder(application,
            AppDatabase::class.java, "appDatabase.db")
            .fallbackToDestructiveMigration()
            .build()
    }

    @Provides
    @Singleton
    fun provideUserDAO(database: AppDatabase) = database.userDAO()
}

Explanation

If you are looking for a detailed explanation of this code, consider referencing the following post.
Dependency Injection (DI) in Android: Providing Interfaces (Day6)

For a high-level understanding of the code, the following is implemented above

  1. We have told Dagger2 how to create and provide the database with the provideDatabase method, it's annotated with @Singleton, because naturally, we'd want a single instance of the database being used across the whole app instead of it being recreated again and again which will be resource intensive.

  2. Next, we need to also provide the UserDAO object, since it'll be needed to execute db operations. This is done through the provideUserDAO method.

Now that we have our database module ready, let's go ahead and add it to our app component as follows.

@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, DatabaseModule::class])
interface AppComponent : AndroidInjector<MyApp> {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: Application): AppComponent
    }
}

Injecting into MainActivity

Now since we want to inject this database instance into our MainActivity, we need to also let Dagger2 know about the MainActivity (since it's a framework type class, we cannot directly perform constructor injection here).
PS: If you are still confused, consider reading this post Injecting into Android Framework Classes with Dagger2: (Day 14)

At this point, we'll have an AppModule where we'll annotate the activities which we'd like to inject our database into as follows.

@Module
abstract class AppModule {
    @ContributesAndroidInjector
    abstract fun contributeMainActivity(): MainActivity
}

Now, we can go ahead and field inject our UserDAO into our MainActivity as follows.

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var userDAO: UserDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

All right, so now we are ready to use our userDAO and save some data into our database.

To do this, let's create a simple button, and at the click of a button, we'll create a simple UserData object and try to save it as follows.

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var userDAO: UserDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val insertButton = findViewById<Button>(R.id.insert)

        insertButton.setOnClickListener {
            val data = UserData()
            data.email = "testemail@test.com"
            data.name = "test name"
            data.password = "test password"
            data.uid = "12312"

            CoroutineScope(Dispatchers.IO).launch {
                userDAO.insertUsers(userData = data)
            }
        }
    }
}

Note:

CoroutineScope(Dispatchers.IO).launch { ... }: A new coroutine is used with the IO dispatcher because it is optimized for IO tasks like database operations.

And that's it. Let's run our app and try to click the insertButton to see if it actually saves anything into the database.

Result

And that's it! As you can see, we now have a database entry into the UserData table with the data points we mentioned in MainActivity.

Conclusion

One important thing to note here is that generally the DAOs are NOT directly injected into the Activities, we did that in our above example simply to make the implementation more straightforward and easy to understand.

In our upcoming posts, we'll try to build on top of what we've created here to add more features and third-party libraries like Retrofit & GSON. Till then, happy coding!