Android Room Tutorial Part 2 – The Architecture

In this article, I’ll show you how to decouple Room related logic from the rest of your application and explain the benefits and the drawbacks of this approach. This is the second part in a series of tutorials about Room ORM in Android (part 1).

In a nutshell, my approach to Room integration can be summarized as “there should be only one module in the project that depends on Room, and it shouldn’t be the ‘main’ module”. Let me show you how to get there.

Standalone Gradle Module for the Database

First, I extract a new standalone module called database. Then I add all Room related dependencies into its build.gradle file:

implementation "androidx.room:room-runtime:$roomVersion"
ksp "androidx.room:room-compiler:$roomVersion"
// kapt "androidx.room:room-compiler:$roomVersion" // use this instead of ksp if you haven't migrated to KSP yet
// apt "androidx.room:room-compiler:$roomVersion" // use this in Java projects
implementation "androidx.room:room-ktx:$roomVersion" // use this for Coroutines support

Just like in the first part of this series, I define a simple Room entity to use as a tutorial (unless stated otherwise, all the classes that I mention in this post go into database module):

@Entity(tableName = "appMemoryInfo")
data class AppMemoryInfo(
    @field:ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long,
    @field:ColumnInfo(name = "consumedMemory") val consumedMemory: Float,
)

Then add my subclass of RoomDatabase, which has AppMemoryInfo as entity and provides AppMemoryInfoDao:

@Database(
    entities = [AppMemoryInfo::class],
    version = 1
)
abstract class MyRoomDatabase : RoomDatabase() {
    abstract val appMemoryInfoDao: AppMemoryInfoDao
}

The declaration of AppMemoryInfoDao:

@Dao
interface AppMemoryInfoDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(entity: AppMemoryInfo)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(entities: List<AppMemoryInfo>)

    @Query("SELECT * FROM appMemoryInfo")
    suspend fun fetchAll(): List<AppMemoryInfo>
    
    @Query("SELECT * FROM appMemoryInfo WHERE id = :id")
    suspend fun fetchById(id: Long): AppMemoryInfo

    @Query("DELETE FROM appMemoryInfo")
    suspend fun deleteAll()
}

And that’s it! Now I have Room set up in a standalone module. However, I’m not done yet.

While all Room related classes reside in the database module now, other modules that would like to use MyRoomDatabase class would need to declare Room as a dependency. This violates the goal of having Room fully encapsulated in one module, so we have more work to do.

Hiding Room Classes from the External World

Now I create a wrapper class around MyRoomDatabase and call it MyDatabase:

class MyDatabase(context: Context) {
    private val myRoomDatabase: MyRoomDatabase

    init {
        myRoomDatabase = Room.databaseBuilder(
            context,
            MyRoomDatabase::class.java,
            DatabaseConstants.DATABASE_NAME
        ).build()
    }
    
    val appMemoryInfoDao get() = myRoomDatabase.appMemoryInfoDao
}

Note that the public API of MyDatabase is equivalent to MyRoomDatabase, but it doesn’t extend any Room’s class. This means that external clients can use MyDatabase without depending on Room:

 myDatabase.appMemoryInfoDao.upsert(AppMemoryInfo(0, consumedMemory))

You might wonder: “Wait a second! External clients use AppMemoryInfo and AppMemoryInfoDao classes, which carry Room annotations. Wouldn’t these clients still need to know something about Room?”. Luckily, since Room does all its magic through code generation at compile time, these annotations aren’t used by the clients in any way, so they don’t force Room dependency on them.

All in all, this simple trick with MyDatabase wrapper allowed us to encapsulate Room dependencies inside one module. You can even make MyRoomDatabase internal class.

Benefits of Encapsulated Room Dependency

I found the following benefits in the structure described above:

  1. Room is a KSP plugin (or an annotation processor), so, by encapsulating it in a single module, I might avoid the need to use KSP in other (client) modules.
  2. Having all database related logic in one module makes my codebase safer. If there are any changes in that module, I know that they’re related to the database, so I need to be very careful and review the changes for e.g. potential migration requirements.
  3. In larger projects, database related code can grow considerably. By extracting it into a standalone module I can potentially speed up the builds.

Drawbacks of Encapsulated Room Dependency

As with every architectural decision, there are also drawbacks that need to be taken into account:

  1. Extracting standalone module for Room slightly increases the complexity of the project during the initial stages of development.
  2. Since I put all Room entity classes inside database module, this module will have semantic dependency on all other app’s features that use the database. This might make database module pretty big and goes against the popular “modularization by feature” paradigm.

Data Structures Duplication

While not strictly necessary, this approach calls for Room entities inside the database module to be used for serialization and deserialization exclusively. In other words, you should use AppMemoryInfo class when working with the database, but avoid using it for building the UI, for example. Instead, you should define additional set of data structures for the general use in the app. This, of course, requires more effort and can lead to some duplication.

Now, while having a standalone set of data structures for database related operations exclusively indeed requires more work, I don’t consider this to be a real drawback. In fact, I think this is a best practice.

See, when you use the same class to define the database schema and for general use in other parts of the app, sooner or later, the requirements for these use cases will diverge. At that point, you’ll have to either evolve AppMemoryInfo into Frankenstein data structure to accommodate both use cases (which can lead to major long-term issues), or refactor to two standalone data structures. I prefer to just bite the bullet right away and go with two sets of data structures from the onset. It adds work and might look like a waste initially, but, in my experience, pays off relatively quickly as the codebase grows and requirements evolve.

Conclusion

The approach to Room integration that I described in this article is simple and straightforward. Despite that, in my experience, it can bring major long-term benefits, so I highly recommend considering it for your project. You can see a full working implementation of this pattern in my open-sourced TechYourChance app. As of the time of this writing, there isn’t much database related code there, but it still shows you all the relevant pieces at work.

Thank you for reading and please leave your comments and questions below.

Check out my premium

Android Development Courses

2 comments on "Android Room Tutorial Part 2 – The Architecture"

  1. Thanks for the article. As usual, very good content presented superbly!

    Taking into account your previous articles (particularly this one about project package structures – https://www.techyourchance.com/popular-package-structures/), I’m a bit surprised you prefer having Room (or any other API) encapsulated in a module, since that reminds me to “Package by Layer”, which clashes with “Package by Feature”. If I’m not wrong, the latter was your preferred option when organising your projects.

    The current encapsulation approach feels a bit weird, since, as you’ve said, the module can grow enormously and it will get rebuilt probably at all times, since any module change could potentially affect it.

    So, could you please elaborate a bit more about your decision or maybe talk about an actual project you’ve found it useful for?

    Reply
    • Hey,
      Yes, this is indeed “modularize by layer” approach. As I wrote in the article, one of the drawbacks is that this layout will make it harder to implement full “modularize by feature” approach. It’s a trade-off. So far, it worked alright for me.
      As for rebuilding, it’s the other way around. Since this module contains just the database-related stuff, it’ll need to be rebuilt only when database changes. I assume that this should be relatively rare occurence in a mature Android project, so, most of the time, this layout will give build time advantage.

      Reply

Leave a Comment