Android Room: The good, the bad, the ugly

Reading Time: 5 minutes

In these series of posts (don’t worry, they’ll be only 3), I would like to dig a bit deeper on the experience of using Room for Android and how it compares to other existing technologies. Please, note that I won’t explain how everything works and how to wire together all the components – there are plenty of tutorials for that – but I will focus on some aspects of the library and on how it feels to use it every day. Moreover, we will try to give a few insights on why you might want or not to use it.

I think it’s best to hear first the bad news and then the good ones, so I’d like to start these series highlighting a few parts of Room which we didn’t find great. Let’s start!

TL;DR: Room is an improvement but we still need to write tons of SQL for trivial cases. Migrations became a chore instead of a pain: a good trade-off.

What is Android Room

Room is an ORM, which Developers can leverage to save and retrieve data to a database by simply interacting with normal objects, removing DBMS specific code and abstracting (some parts of) SQL. This library was created specifically by Google for Android Developers, in order to decrease the amount of boilerplate code that they needed to write to work with the only native DBMS available on the platform (SQLite) and ease some of the pain points that they had with it.

The Ugly

The first time I understood what an ORM is, my inner self started screaming: “NO MORE SQL!”.
Don’t get me wrong, not because it’s a bad language or anything, but simply because not having business logic inside your queries means better portability and also because, it is super convenient just having to deal with objects since they are a natural part of the language. However, with Room, you still have to write quite some SQL, which I think makes it a bit more verbose than necessary. Let’s suppose that we want to query the database to retrieve a product based on its id – a pretty common operation and let’s see a part of the needed code:

@Dao
interface ProductDao{
   @Query("SELECT * FROM `Product` WHERE id= :id")
   fun findById(id: Long): LiveData<Product>
}

Even without much familiarity with the Kotlin language (what are you waiting for? ?), the above interface describes a simple DAO, which is the gateway to store and retrieve information from the database. On the first line, we can observe the @Dao annotation, which is used by the Room’s processor to generate an actual implementation of this class, where the SQL Statements are prepared for us and then, in some other part of the code, you can simply call ProductDao.findById(orderId). However, I would like to show you how the same goal can be achieved in the Spring framework (most popular backend framework of the JVM world):

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
   Product findById(Long productId);
}

At first, it might seem there is even more code than Room’s one: the programmer needs to extendJpaRepository and Long has to be explicitly set as a secondary type for no obvious reason. However, you could notice that here the programmer didn’t have to write any SQL statement because the framework automatically does it only for the methods contained in the class. In practical terms, how the problem gets solved is similar: another structure called Repository, or Dao in Room, is created and it is its responsibility to save and retrieve objects into or from the database.

If you don’t mind, I would like to show you another approach to the problem, which first originated outside the JVM world and far away from the mobile one: Ruby on Rails. They use inheritance to make all the necessary method available, like this:

class Product  < ApplicationRecord
end

As long as the Product class inherits from `ApplicationRecord`, the `find` method will just be there for that object and, by default, it will do what everyone expects, with no SQL written by the developer. Naturally, due to the constraints of the JVM this is not directly translatable in Java or Kotlin but, a very popular Android Library, called ActiveAndroid achieved something similar. Let’s have a look:

@Table(name = "Product")
class Product: Model {
}

// later somewhere else:
fun getProd(orderId: Long) = new Select()
      .from(Product.class)
      .where("id = ?", orderId)
      .execute();
}

The id field is automatically generated by the annotation processor, together with the save() method (and many others), without writing any SQL statement. The library is very popular and active on Github, counting more than 4500 stars, 23 contributors and many issues solved on a daily basis.

So, why do we have to write SQL in Room?

I guess there are two main reasons why the designers of Room decided to not adopt the model of ActiveAndroid and opted for the DAO/Repository approach. First, it’s important to note that ActiveAndroid relies also on inheritance, which makes the system less flexible and we all know that composition should be favored, right? But, more importantly, by going for that approach the system might have ended up generating more methods than necessary, thus taking a toll on the DEX method count limit, which is always there to bite us poor Android Developers.

Personally, I would have really enjoyed a hybrid approach, something where methods like `findBySomeProperty` can be just defined and the library figures out by itself the query (similarly to what Spring does) so that a developer should resort to the @Query annotation only when truly necessary, especially because the library already figure out the queries for @Update, @Insert and @Delete

Another Small Ugly part

There is another minor annoyance with Room regarding migrations, which is probably just a nuisance, but since we are here. At first, when I saw that migrations were supported, I was so happy that I jumped on the desk (followed by one of my back-end co-worker helping me getting down since the height gave me nausea), but later on I found out that I had to write the SQL for both creating and altering tables, and whilst the latter is more understandable, I found the former a bit of a chore. Especially due to the fact that the annotation processor generates the queries anyway, but you cannot use it directly: there is a line which to add to the build.gradle file, which tells the annotation processor to write the schema in JSON format in a user-defined folder and then you need to copy the produced query from the schema in your migration code. It seems simple enough but let’s have a look at the generated JSON:

{
   "formatVersion": 1,
   "database": {
       "version": 2,
       "identityHash": "some hash",
       "entities": [
           {
               "tableName": "Product",
               "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, ...)"
               "fields": [
                   {
                       "fieldPath": "id",
                       "columnName": "id",
                       "affinity": "INTEGER",
                       "notNull": true
                   }
               ]
           }
       ]
// some other stuff

As you can see, the generated query contains `${TABLE_NAME}`, referencing to the above variable instead of the actual table name, thus the developer has to replace it whenever it pastes the query into the migration code. Now, imagine that you created more than a few tables and you can understand how much this copy and paste could become a chore even if it nothing major. We are planning to create some kind of library (or utility) to fix this eventually ( if Google doesn’t) so stay tuned!

Thanks for arriving down here

This will be the end for part one! Be sure to not miss the next part: “The Bad” and to let us know what you think down in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *