Coming soon: Kotlin Programming: The Big Nerd Ranch Guide, Second Edition
Android Ranch NewsKotlin has been changing at a steady pace over the years, and more and more developers are choosing Kotlin as their language of choice....
Room is a new way to save application data in your Android app announced at Google I/O this year.
It’s part of the new Android Architecture components, a group of libraries from Google that support an opinionated application architecture. Room is offered as a high-level, first-party alternative to Realm, ORMLite, GreenDao and many others.
Room is a high-level interface to the low-level SQLite bindings built into Android, which you can learn more about in Android’s documentation. It does most of its work at compile-time by generating a persistence API on top of the built-in SQLite API, so you don’t have to touch a single Cursor
or ContentResolver
. Room lets you use SQL where it’s most powerful, for query building, while taking care of schema definition and data manupulation for you.
First, add Room to your project. After that, you’ll need to tell Room what your data looks like. Suppose you start with a simple model class that looks like this:
public class Person {
String name;
int age;
String favoriteColor;
}
To tell Room about the Person
class, you annotate the class with @Entity
and the primary key with @PrimaryKey
:
@Entity
public class Person {
@PrimaryKey String name;
int age;
String favoriteColor;
}
With those two annotations, Room now knows how to make a table to store instances of Person
. One thing to note when you’re setting up your models is that every field that’s stored in the database needs to either be public or have a getter and setter in the usual Java Beans style (e.g. getName()
and setName(String name)
).
Your Person
class now has all the information Room needs to be able to create the tables, but you don’t have a way to actually add, query, or delete a Person
’s data from the database.
That’s why you’ll have to make a data access object (DAO) next.
The DAO is going to give an interface into the database itself, and will take care of manipulating the stored Person
data.
Here’s a simple DAO for the Person
class from before:
@Dao
public interface PersonDao {
// Adds a person to the database
@Insert
void insertAll(Person... people);
// Removes a person from the database
@Delete
void delete(Person person);
// Gets all people in the database
@Query("SELECT * FROM person")
List<Person> getAllPeople();
// Gets all people in the database with a favorite color
@Query("SELECT * FROM person WHERE favoriteColor LIKE :color")
List<Person> getAllPeopleWithFavoriteColor(String color);
}
The first thing to notice is that PersonDao
is an interface, not a class.
This interface is straightforward enough that Room could write the class for you, so that’s exactly what it does!
The other interesting detail is the SQL statements in the @Query()
annotations.
The SQL statements tell Room what information you want to get out of the database, and they can be as simple or as complicated as you want.
They’re also validated at compile-time, so you can have confidence that your syntax is correct without having to run your app to see if you get a runtime exception.
So if you change the method signature of List<Person> getAllPeopleWithFavoriteColor(String color)
to List<Person> getAllPeopleWithFavoriteColor(int color)
, Room will give you a compile error:
incompatible types: int cannot be converted to String
And if you make a typo in the SQL statement, like writing favoriteColors
(plural) instead of favoriteColor
(singular), Room will also give you a compiler error:
There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: favoriteColors)
You may notice that you can’t get an instance of the PersonDao
because it’s an interface.
To be able to use your DAO classes, you’ll need to make a Database class.
Behind the scenes, this class will be responsible for maintaining the database itself and providing instances of the DAOs you declared before.
You can create your database class with just a couple of lines of code:
@Database(entities = {Person.class /*, AnotherEntityType.class, AThirdEntityType.class */}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract PersonDao getPersonDao();
}
This describes the structure of the database, but the database itself will live in a single file.
To get an AppDatabase
instance saved in a file named populus-database
, you’d write:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "populus-database").build();
If you wanted to get all of the Person
data that’s in the database, you could then write:
List<Person> everyone = db.getPersonDao().getAllPeople();
Keep in mind that RoomDatabase
instances are expensive, so you’ll want to create just one and use it everywhere.
Dependency injection makes it easy to manage this instance.
Unlike most ORMs, Room uses an annotation processor to perform all of its data persistence magic.
This means that neither your application classes nor model classes need to extend from anything in Room, unlike many other ORMs including Realm and SugarORM.
As you saw when making mistakes with the @Query()
annotations above, you also get compile-time validation of your database schema and SQL query statements, which can save you a lot of hassle compared to the runtime exceptions you’d get if you were using the native SQLite interface.
Room also allows you to observe changes to the data by integrating with both the Architecture Components’ LiveData API as well as RxJava 2.
This means that if you have a complicated schema where changes in the database need to appear in multiple places of your app, Room makes receiving change notifications seamless.
This powerful addition can be enabled with a one-line change in your DAO.
All that you have to do is change your @Query
methods to return a LiveData
or Observable
object.
For example, this method:
@Query("SELECT * FROM person")
List<Person> getAllPeople();
becomes this:
@Query("SELECT * FROM person")
LiveData<List<Person>> /* or Observable<List<Person>> */ getAllPeople();
The biggest limitation of Room is that it will not handle relationships to other entity types for you automatically like some other ORMs will.
That means if you want to keep track of people’s pets like this:
@Entity
public class Person {
@PrimaryKey String name;
int age;
String favoriteColor;
List<Pet> pets;
}
@Entity
public class Pet {
@PrimaryKey String name;
String breed;
}
Then Room will give a compiler error since it doesn’t know how to store the relationship between a Person
and its Pets
:
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
The compiler error suggests a type converter, which converts objects into primitives that can be directly stored in SQL.
Since List
cannot be reduced into a primitive, you’ll need to do something different.
This is a one-to-many relationship, where one Person
can have many Pet
s.
Room can’t model relationships like this, but it can handle the reverse relationship – each Pet
has a single owner.
To model this, remove the pets
field in Person, and add an ownerId
field to the Pet
class as shown:
@Entity
public class Person {
@PrimaryKey String name;
int age;
String favoriteColor;
}
@Entity(foreignKeys = @ForeignKey(
entity = Person.class,
parentColumns = "name",
childColumns = "ownerId"))
public class Pet {
@PrimaryKey String name;
String breed;
String ownerId; // this ID points to a Person
}
This will cause Room to enforce a foreign key constraint between the entities.
Room won’t infer one-to-many and many-to-one relationships, but it gives you tools to express these relationships, fulfilling its promise as a high-level interface to SQLite.
To get all of the pets owned by a specific person, you can use a query that finds all pets with a given owner ID.
For example, you can add the following method to your DAO:
@Query("SELECT * FROM pet WHERE ownderId IS :ownerId")
List<Pet> getPetsForOwner(String ownerId);
If you want the pets
field to stay in the Person
model object, then there’s a few more steps.
The first is to tell Room to ignore the pets
field – otherwise you’ll get the previously mentioned compiler error again.
You can do that by annotating the pets
field with the @Ignore
annotation like this:
@Entity
public class Person {
@PrimaryKey String name;
int age;
String favoriteColor;
@Ignore List<Pet> pets;
}
This tells Room not to read or write that field to and from the database.
This means that every time you get a Person
from the DAO, the pets
field will be set to null
, so you’ll have to populate it yourself.
One way to do this is to have your Repository query for all Pets
that a Person
owns and set the pets
field before passing a Person
to the rest of your application.
If you’ve already set up data persistence in your app and are happy with it, then you should keep it.
Every ORM and the native SQLite implementation are going to keep working just as they have before – Room is just another data persistence option.
If you’ve been using SQLite or are considering using it, though, you should absolutely try Room.
It has all the power you need to perform advanced queries while removing the need for you to write SQL statements to maintain the database yourself.
Alpha 3 of Room is available right now, but it’s very likely that the APIs will change before the final release, so you may want to avoid using it in a production app until then.
It’ll be available with the rest of the Architecture Components when they go stable in the coming months.
Kotlin has been changing at a steady pace over the years, and more and more developers are choosing Kotlin as their language of choice....
Data Binding is a powerful library that's built around the idea of keeping your views up-to-date with your model. One of the lesser-known features...
Later this year, Android Q is going to appear on some of the 2.5 billion devices running on Android, so let's take a look...