How to Make DBFlow as Testable as Room, with Kotlin
In the Android database ORM-space, Room-the new guy on the block- is a hot topic and rightfully-so: it focuses on testability via Retrofit-style interface definitions, is supported by the team of Google engineers responsible for the Architectural Components, and is simple to use (it works well with the other components and provides seamless interop).
I wrote this article to show you can still make your code testable with DBFlow even if the main implementation does not require you do so.
When creating DBFlow I wanted to provide a lightning fast, flexible, and feature-rich library that doesn’t limit developers to how they should implement it. So far I think that has worked. On the other hand, Room is pretty opinionated and wants you to follow a strict set of supported features.
DBFlow also provides a simple implementation and is supported by an active community. Some of the core motivators behind its design are speed and flexibility via a feature-rich library that doesn’t limit developers to a particular design. So what about testability? A data layer backed by DBFlow can be highly testable if the appropriate design paradigm is used. The flexibility offered by the library often leads newcomers to develop systems that are difficult to test, and hopefully this discussion will help you avoid this problem. Specifically, I will flex some of DBFlow’s muscles and show you how you can imitate Room’s DAO classes to make your app testable with Dagger2.
Before continuing, I will define some concepts used in this article:
Entity: A class that represents the structure of a table. Each corresponding field captured in the class is a column and each instance of the class is a corresponding row in the Database.
DAO: Data Access Object. The DAO is an abstraction away from the real implementation that actually interacts with the database.
Dagger2: A dependency injection framework developed by Google. It uses annotation processing to validate and generate all of the boilerplate normally required.
Here are the steps to achieve our goal:
- Define our Entities
- Define our DAO
- Build our Database
- Inject our DAO into Dagger2
Define our Entities
In Room we define:
@Entity
class User(@PrimaryKey var id: Int, var firstName: String, var lastName: String)
With DBFlow we define:
@Table(database = AppDatabase::class, allFields = true)
class User(@PrimaryKey var id: Int = 0, var firstName: String = "", var lastName: String = "")
Room is eager, meaning all fields are counted by default, unless you specify @Ignore
, while DBFlow is lazy unless you specify allFields=true
.
Notice we specify the database class in the @Table
annotation. With DBFlow, a class can only represent the schema of a table for a single database. In contrast, Room allows a single class with the @Entity
annotation to represent the table schema across multiple databases.
DAO
Unlike Room, DBFlow does not have a built-in representation for a Data Access Object (DAO). DBFlow provides something similar with theModelAdapter
class which is generated for every table in the database. ModelAdapter
are responsible for marshaling data between the database and our models. We can mock these classes for testing purposes; however, it is very difficult to do so because there are many methods that use Android-specific DB classes such as ContentValues
, DatabaseStatement
, FlowCursor
and more.
Enter Room with its DAO inside:
@Dao
interface UserDao {
@Query("SELECT * FROM User")
fun loadUsers(): List<User>
@Insert
fun insertUsers(userList: List<User>)
@Update
fun updateUsers(userList: List<User>)
@Delete
fun deleteUsers(userList: List<User>)
}
In this example we have abstracted away any knowledge of the internal workings of the DAO (vs. the ModelAdapter). Now all we have to do is mock this class and supply Mockito doReturn()
or other stubbing mechanisms for the methods we care about in the DAO.
In DBFlow using Kotlin and the DBFlow Kotlin Extensions library, we can produce code that is similar to the class above:
interface UserDAO {
fun loadUsers() = (select from User::class).list
fun insertUsers(userList: List<User>) = modelAdapter<User>().insertAll(userList)
fun updateUsers(userList: List<User>) = modelAdapter<User>().updateAll(userList)
fun deleteUsers(userList: List<User>) = modelAdapter<User>().deleteAll(userList)
}
From this example and the beauty of default interface definition methods in Kotlin, we can define implementation details in the same concise way that Room provides. Also since DBFlow’s wrapper language is compile-time safe, we get similar benefits to Room’s compile time checking. The only difference here is if the User
table was not defined, DBFlow would throw a RuntimeException
whereas Room would throw a compile time exception.
Also instead of using String
queries, we utilize the wrapper language in DBFlow which will give us good refactoring and find usages support.
Room’s DAO do not allow you to run the operations on the main thread by default (with good reason and there’s a workaround). DBFlow has no such restriction, although its encouraged to use async transactions instead:
interface UserDAO {
fun loadUsers() = (select from User::class).async
}
You might be asking, “What about RX Support?” Room supports Flowable
and Publisher
. Given this interface method that loads all User
:
@Dao
interface UserDao {
@Query("SELECT * FROM User")
fun loadUsers(): Flowable<List<User>>
}
With DBFlow you get support for both RXJava 1 and 2 via add-on artifacts. Using the DBFlow-RX + Kotlin RX extensions library we define our DAO using RXJava2:
interface UserDAO {
fun loadUsers() = (select from User::class).rx().observeOnTableChanges()
.observeOn(Schedulers.io()).map { it.queryList() }
}
Now that we have our DAO, we build our database.
Database
To tie the DAO and models together, we must define our database.
In Room you collect all of the tables and DAOs and define them in a class:
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract val userDao: UserDao
}
Room only generates code for DAOs and @Entities
defined in the database. If you include @Entity
or @DAO
that do not get used, corresponding implementations are not generated.
DBFlow, on the other hand, requires @Table
to define their database, so there are no empty generated classes, as well. The DAO is not part of the library so we do not need to specify it. With this, the database definition is as simple as possible:
@Database(version = 1)
object AppDatabase
In order to prevent making the database file large with defined entities in DBFlow, I decided to require models to define what database they belong to. Doing it this way ensures every @Table
corresponds to a database. However, this means only whole DB’s can be shared across modules, not individual model classes. Conversely, Room’s implementation allows you to reuse models in different databases and different modules.
Inject DAO into Dagger2
Using Dagger2 for dependency injection, we can make our apps very testable, while providing dependencies to our classes based on scopes and compile-time verification. I will not walk through setting up Dagger2, but I will mention how we can now inject our DBFlow DAO interface and make our app fully testable.
Using Dagger2, we can inject our DAO. With Room you might define a module named DBModule
that provides our DAO implementations:
@Module
class DBModule { @Provides @Singleton
fun appDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "room-db").build() @Provides @Singleton
fun userDAO(db: AppDatabase) = db.userDAO}
With DBFlow it would be slightly different:
@Module
class DBModule {
val userDAO
@Provides @Singleton
get() = object : UserDAO {}}
Since DBFlow utilizes a global singleton instance for each database (and takes a database parameter at most operations), we don’t need to pass the DB into the DAO here.
In this article I am using MVVM (Model-View-ViewModel) as the main paradigm for implementation. This could just as easily work with MVP (Model-View-Presenter).
Now we can utilize it in our UserViewModel
class:
class UserListViewModel
@Inject constructor(private val userDAO: UserDAO) : ViewModel() {
fun loadUsers() {
userDAO.loadUsers().subscribe {
// load up users and listen for changes
// also want to dispose it when you don't need it anymore } } }
Note: I have skipped a lot of the work in making the injection work for ViewModels. There is a really great example project here explaining how to inject ViewModels with the Architecture Components library easily.
With this abstraction, we can easily mock in tests — UserDAO
is just an interface.
Conclusion
You can achieve the same testability as Room when you combine Kotlin and DBFlow into interface definitions. With DBFlow’s flexibility and rich feature-set, you can write testable and amazing apps. It is all up to you to design and implement a testable architecture and I hope this article inspires you.
For more on mobile development, web, and design, checkout the Fuzz Medium.