In the ORM-space Android database, Room-the new guy on the block-is a hot and true-so topic: it focuses on testability capabilities via the Retrofit-style interface definition, powered by a team of Google engineers responsible for the Architecture Components, and easy to use (works well with other components and provides unlimited interop).
I am writing this article to show that you can still make your code testable with DBFlow even if the main implementation does not require you to do so.
When creating DBFlow, I want to provide a fast, flexible, feature rich flash library that does not limit developers on how they should implement it. So far I think it has worked. On the other hand, Room is quite opinionated and wants you to follow a series of strict support features.
DBFlow also provides a simple implementation and is supported by an active community. Some of the main motivators behind the design are speed and flexibility through feature-rich libraries that do not limit developers to specific designs. So what about testability? Data layers supported by DBFlow can be very easy to test if the proper design paradigm is used. The flexibility offered by libraries often causes 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 the DBFlow muscles and show you how you can emulate DAO Room classes to make your application testable with Dagger2.
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:
With DBFlow we define:
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.
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, FlowCursorand more.
Enter Room with its DAO inside:
In this example, we have omitted the knowledge of the internal workings of DAO (vs. Adapter Model). Now all we have to do is mock this class and supply Mockito doReturn () or any other stubbing mechanism for the method we're interested in DAO.
In DBFlow using Kotlin and DBFlow Kotlin Extensions library, we can generate code similar to the above class:
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:
You might be asking, “What about RX Support?” Room supports Flowable and Publisher. Given this interface method that loads all 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:
Now that we have our DAO, we build our 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:
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:
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:
With DBFlow it would be slightly different:
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:
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.
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.
source : https://android.jlelse.eu