background

Android’s modular architecture has long been popular, and using databases in modules is a common requirement; As we all know, Android provides Room database architecture components to greatly improve the ease of writing databases.

So, how do you use the Room database in a modular architecture?

Let’s focus on a scenario. For example, a news application has shell App (appModule), AccountModule, and NewsModule. The account module and news module need to use the database;

Common practices and advantages and disadvantages;

There are probably a lot of ways to do this, but here are two common ways;

1. DBModule scheme

How to do it: Make a mess of all Model, DAO, and Database initializations and singletons in this module; Other modules that need to use the database depend on this module; As shown below:

Advantages:
  • Low overhead: there is only one database singleton globally;

  • Easy to write: just use the database module, rely on the DBModule, and then throw the Model and DAO into it.

Disadvantages:
  • High coupling: inconsistent with the open and close principle, every change to the DBModule may affect the functions that depend on the Module;

  • Fuzzy code boundaries: data and business logic from multiple modules are mixed into the same module;

  • Low reusability: Multiple modules have another function, that is, modules can be reused for multiple applications, but the database related logic is in the same module, obviously not easy to migrate;

2. Multi-database scheme

Practice: each set up a mountain way, each use of DB sub-module to maintain a database; Inherit RoomDatabase to implement a separate database; The diagram below:

Advantages:
  • No coupling, independent code boundary, high reusability;
Disadvantages:
  • Expensive: The official Room documentation notes that each database instance is “very expensive”; Therefore, when multiple modules have DB, there may be a large problem of resource overhead.

I think about the scheme

Let’s first sort out the problems we have encountered or the points we hope to achieve:

  1. Low coupling, easy module migration;
  2. Code boundaries are clear, with each module focusing on its own Model and DAO;
  3. Minimal overhead: singletons;

To get straight to the solution: Database singletons are in the main Module; Each submodule maintains its own Model and DAO; Since the main module also relies on other child modules, Database declarations can get the models and DAOs of each child module.

Now the problem to be solved is the instantiation of the sub-Module DAO. The DAO instantiation method in Room is obtained through the instance of DataBase. Therefore, you can ask the main Module for DAO when the sub-module needs it.

The approximate relationship diagram is as follows:

However, the two dashed lines pointing to apps in the AccountModule and NewsModule are clearly problematic. We can only rely on submodules as shell apps, not vice versa;

So now there is a dependency direction to solve, sub-module can not rely on the main module, the direct way to get DB instance, so you can expose an interface out, in the main Module to implement the interface, return the corresponding DAO instance can be;

The child in the module:

AccountRoomAccessor, the access layer of the AccountModule, defines an interface. NewsModule similar;

object AccountModuleRoomAccessor {
  var onGetDaoCallback: OnGetDaoCallback? = null

  internal fun getUserDao(a): UserDao {
    if (onGetDaoCallback == null) {
    throw IllegalArgumentException("onGetDaoCallback must not be null!!")}returnonGetDaoCallback!! .onGetUserDao() }interface OnGetDaoCallback {
    fun onGetUserDao(a): UserDao
  }
}
Copy the code
Shell in the App:

Initialize the database and declare the DAO and Model of the sub-module:

@Database( entities = [ UserModel::class, NewsDetailModel::class, NewsSummaryModel::class ], version = 1, exportSchema = false )
abstract class TestDataBase : RoomDatabase() {
    abstract fun userDao(a): UserDao
    abstract fun newsSummaryDao(a): NewsSummaryDAO
    abstract fun newsDetailDao(a): NewsDetailDAO
}
Copy the code

Implement the subModule interface and return the DAO instance:

class App: Application() {... .override fun onCreate(a) {
        super.onCreate() ... . AccountModuleRoomAccessor.onGetDaoCallback =object : AccountModuleRoomAccessor.OnGetDaoCallback {
            override fun onGetUserDao(a): UserDao {
                return DBHelper.db.userDao()
            }
        }

        NewsModuleRoomAccessor.onGetDaoCallback = object : NewsModuleRoomAccessor.OnGetDaoCallback {
            override fun onGetNewsDetailDAO(a): NewsDetailDAO {
                return DBHelper.db.newsDetailDao()
            }
            override fun onGetNewsSummaryDAO(a): NewsSummaryDAO {
                return DBHelper.db.newsSummaryDao()
            }
        }
    }
}
Copy the code

This will solve the above problems;

When the Module needs to be migrated, although it is not as fast as plan B, the Model and DAO do not need to be manually copied, but only need to register in the Database of another App.

But… Another problem is that for implementers, each module needs to define its own access layer, expose an interface, and then implement it is a bit tedious…

So, is it possible to do this layer in a way that automatically generates code? The answer, of course, is yes.

I wrote a code generation library based on APT, which will automatically traverse the @DAO annotation in each module, and then automatically generate the access layer; Yes, the above AccountModuleRoomAccessor is automatically generated;

The code is visible: github.com/linkaipeng/…

The instance is visible in the demo repository

So far, that’s all I have to think about this plan.

In addition, there are some deficiencies

It is better to put the database related code in each module. However, shell apps rely on submodules in a way that implementation does (because database declarations need to be imported into DAO and Model).

But the ideal dependency would be through runtimeOnly; The shell App relies on runtimeOnly. The shell App relies on implementation to rely on child modules that have database requirements. In this way, the shell App can be completely separated from the sub-module to avoid access to the sub-module in the shell App;

Or there are other better solutions, we can discuss.