ZOFTINO.COM android and web dev tutorials

Android Persistence Library Room

Room is a framework that can be used to read data from and write data to SQLite database. Using Room, db operation can be performed easily as it offer annotations and compile time validation of SQL queries, and you don’t need to worry about boiler plate code for converting SQL to JavaObject using Room. Most importantly, you can build reactive and well performing apps using Room, LiveData and ViewModel.

SQLite database is part of android system and is useful for caching data locally on device. While android already has API, SQLite API, to access SQLite database and perform database operation, using SQLite APi has its own disadvantages. SQLite API is a low level, requires more time and effort to build apps. You need to handle raw SQL and there are no compile time SQL validations with SQLite API.

Room persistence library is provided as part of android architecture components. Other android architecture components include LiveData and ViewModel. LiveData is lifecycle aware observable and ViewModel is also lifecycle aware component. Instance of ViewModel can exist till activity or fragment is destroyed and it can be reused after restarting activity in response to device rotation.

Room, LiveData and ViewModel can be used together to develop apps which can be easily maintainable, testable and enhanced. I’ll explain components and features of room and show how to use Livedata, ViewModel and Room with an example.

Room Dependencies

First add google maven repository to your project build.gradle file.

repositories {
    jcenter()
    maven { url 'https://maven.google.com' }
}
 

You need to add below dependencies to use room library in your project.

compile "android.arch.persistence.room:runtime:1.0.0-alpha3"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha3"

If you use room with live data and view model, you need to add below dependencies as well.

 compile "android.arch.lifecycle:runtime:1.0.0-alpha3"
compile "android.arch.lifecycle:extensions:1.0.0-alpha3" 

Room Components

To create data access component using room in your app, you need to create three types of classes: Database, Entity and DAO. Database class holds database, Entity classes defines data with each type of entity represents a table in database, DAO defines methods to access database.

All you need to do to use room is that define these three classes with annotations and room will take create of creating database, databse connection, tables, sql, and converting cursors to java objects.

Defining Room Entity

Entity class is a POJO with fields and getter and setter methods. You need to use Entity annotation to mark a class as room entity. Room provides several annotations to define entities.

  • PrimaryKey: marks a field as primary key field.
  • ForeignKey: Sets foreign key constraint.
  • Ignore: Ignores marked field, won’t be part of table.
  • ColumnInfo: Specifies column name in database, instead of using field name as column name.
  • Index: Creates index to speed up queries.
  • Embedded: Marks a field as embedded that makes all subfields of the embedded class as columns of entity.
  • Relation: Specifies relations, useful to fetch related entities.

Room Entity Example

Below entity uses primary key, ignore and embedded annotations.

 @Entity
public class Person {
    private String name;
    @PrimaryKey
    private String mobile;
    private String email;
    @Embedded
    private Address address;
    private String type;
    @Ignore
    private String middleName;
} 

Defining Room DAO

DAO is an interface which is marked with DAO annotation and it contains method definitions related to accessing database. You need to mark methods with annotations such as Insert, Delete, Update, and Query, based on the type of database operation they perform.

Room DAO Example

Example shows how to define DAO using insert, delete, update, and query annotations.

@Dao
public interface PersonDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public long insertPerson(Person person);
    @Update
    public void updatePerson(Person person);
    @Delete
    public void deletePerson(Person person);
    @Query("SELECT * FROM person")
    public LiveData<List<Person>> getAllPersons();
    @Query("SELECT * FROM person where mobile = :mobileIn")
    public LiveData<Person> getPersonByMobile(String mobileIn);
    @Query("SELECT * FROM person where city In (:cityIn)")
    public List<Person> getPersonByCities(List<String> cityIn);
}

Defining Room DataBase Component

Room database component is an abstract class which is marked with Database annotation. Room database component definition includes list of entities and DAOs.

 @Database(entities = {Person.class}, version = 1)
public abstract class PersonDatabase extends RoomDatabase {
    public abstract PersonDAO PersonDatabase();
}
 

Room DataBase Instance

After you define entities, DAOs, and database classes, you can instantiate database using Room.databaseBuilder. It takes application context as parameter.

Since creating database object is expensive, you need to make sure that only one instance of database is created in your app. The best way to make sure that app can’t create more than one instance of a class is by using singleton pattern as shown below.

public class DatabaseCreator {

    private static PersonDatabase personDatabase;
    private static final Object LOCK = new Object();

    public synchronized static PersonDatabase getPersonDatabase(Context context){
        if(personDatabase == null) {
            synchronized (LOCK) {
                if (personDatabase == null) {
                    personDatabase = Room.databaseBuilder(context,
                            PersonDatabase.class, "person db").build();
                }
            }
        }
        return personDatabase;
    }
}

Using Room DAO

After you have implemented singleton class which gives room database instance, you can get implementation of DAO interfaces added to database class. Once you have DAO instances, you can perform defined operations.

PersonDAO personDAO = DatabaseCreator.getPersonDatabase(context).PersonDatabase();

Room DataBase Background Thread

By default, room checks to see whether code is running on the main thread or not and if so, it will throw an exception. It makes sense as database operations can take long time to complete so running them on main thread impacts application performance and user experience. Below sections explain and show how to run room on background thread.

If you need to run room on main thread, for at least testing purpose, you can do by calling allowMainThreadQueries() method on RoomDatabase.Builder.

Running Room Using Thread Pool Executor

As mentioned above, you need to call room DAO methods on background thread. Below codes show how to create executor and use it to call DAO methods on background thread. But this way of calling room on background thread can be used only if you don’t need to perform any operation on main thread after database operation is complete.

    private final Executor executor = Executors.newFixedThreadPool(2);
     private PersonDAO personDAO = DatabaseCreator.getPersonDatabase(context).PersonDatabase();

    public void addPerson(Person p){
        executor.execute(() -> {
	personDAO.insertPerson(p);
        });
    }

Running Room Using AsyncTask

You can run room on background thread using AsyncTask. This way of running room gives you an opportunity to perform tasks on main thread after room dao background operation is complete.

    public void getPersonsByCity(List<String> cities){
        new AsyncTask<Void, Void, List<Person>>() {
            @Override
            protected List<Person> doInBackground(Void... params) {
                //runs on background thread
                return personDAO.getPersonByCities(cities);
            }
            @Override
            protected void onPostExecute(List<Person> personLst) {
               //runs on main thread
                Log.d("", "persons by city "+personLst.size());
                personsByCity.setValue(personLst);
            }
        }.execute();

    }

Running Room Using LiveData

You can use Room with LiveData to make your app reactive to database changes. My previous post explains LiveData with examples. Room DAO methods can return LiveData and these methods run on background thread.

Using Room with LiveData, database operation can start in response to user events and observer of LiveData can update UI with database results, all this can happen without blocking main thread and that too observer is notified every time there is a change to the corresponding data in the database.

In other words, Room and LiveData combination automatically updates UI when there is a change to the data, which LiveData holds, in the database.

DAO returns LiveData

@Query("SELECT * FROM person")
public LiveData<List<Person>> getAllPersons();

Observe LiveData returned from DAO method call

       LiveData<List<Person>> allPersonsLive = personDAO.getAllPersons();
       allPersonsLive.observe(this, new Observer<List<Person>>() {
            @Override
            public void onChanged(@Nullable List<Person> person) {
                if(person == null){
                    return;
                }
                Toast.makeText(MainActivity.this, "Number of person objects in the response: "+person.size(), Toast.LENGTH_LONG).show();
            }
        });

Like in the above example, every time a method of DAO that returns LiveData is called, you will get new LiveData object and you have to add observer to LiveData after the call. To prevent this unnecessary step of adding observer to LiveData every time after DAO method call, you can use transformation as shown below. With transformations, you can add observer only once, for example, in onCreate method of an activity. Below example shows transformation of LiveData returned by room DAO.

    private PersonRepository personRepository = new PersonRepository(this.getApplication());
    private final MediatorLiveData<String> mobileNo = new MediatorLiveData<>();

    //transformation applied so that observer to this LiveData can be added only once
    private final LiveData<Person> personsByMobile = Transformations.switchMap(mobileNo, (mobile) -> {
        return personRepository.getPersonByMobile(mobile);
    });    
    public void setMobile(String mobile){
        mobileNo.setValue(mobile);
    }
    public LiveData<Person> getPersonsByMobile(){
        return personsByMobile;
    } 

Android Room Example

You can download complete room example code from github at https://github.com/srinurp/AndroidRoom

Activity

 package com.zoftino.room;

import android.arch.lifecycle.LifecycleActivity;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends LifecycleActivity{

    private PersonViewModel personViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        personViewModel = ViewModelProviders.of(this).get(PersonViewModel.class);

        //observe LiveData
        //this LiveData instance is not recreated after every db call, so registering observer in onCreate
        //corresponding DAO method returns List of Person objects, not LiveData
        observerPersonListResults(personViewModel.getPersonsByCityLive());

        //corresponding DAO method call returns LiveData
        //Transformations makes it possible to add observer only once to LiveData returned by room DAO
        observePersonByMobile(personViewModel.getPersonsByMobile());
    }
    private void observerPersonListResults(LiveData<List<Person>> personsLive) {
        //observer LiveData
        personsLive.observe(this, new Observer<List<Person>>() {
            @Override
            public void onChanged(@Nullable List<Person> person) {
                if(person == null){
                    return;
                }
                Toast.makeText(MainActivity.this, "Number of person objects in the response: "+person.size(), Toast.LENGTH_LONG).show();
            }
        });
    }
    private void observePersonByMobile(LiveData<Person> personByMob){
        personByMob.observe(this, new Observer<Person>() {
            @Override
            public void onChanged(@Nullable Person person) {
                if(person == null){
                    return;
                }
                ((TextView)findViewById(R.id.name)).setText(person.getName());
                ((TextView)findViewById(R.id.email)).setText(person.getEmail());
                ((TextView)findViewById(R.id.mobile)).setText(person.getMobile());
                ((TextView)findViewById(R.id.lineOne)).setText(person.getAddress().getLineOne());
                ((TextView)findViewById(R.id.city)).setText(person.getAddress().getCity());
                ((TextView)findViewById(R.id.country)).setText(person.getAddress().getCountry());
                ((TextView)findViewById(R.id.zip)).setText(person.getAddress().getZip());
            }
        });
    }
    public void getAllPerson(View view){
        LiveData<List<Person>> allPersons = personViewModel.getAllPersons();
        observerPersonListResults(allPersons);
    }
    public void getPersonByMobile(View view){
        if((TextView)findViewById(R.id.mobile) == null){
            Toast.makeText(this, "Please enter mobile number", Toast.LENGTH_LONG);
            return;
        }
        personViewModel.setMobile(((TextView)findViewById(R.id.mobile)).getText().toString());
    }

    public void getPersonByCities(View view){
        if((TextView)findViewById(R.id.mobile) == null){
            Toast.makeText(this, "Please enter mobile number", Toast.LENGTH_LONG);
            return;
        }
        List<String> cityLst = new ArrayList<>();
        cityLst.add(((TextView)findViewById(R.id.city)).getText().toString());
        personViewModel.getPersonsByCity(cityLst);
    }
    public void addPerson(View view){
        if((TextView)findViewById(R.id.mobile) == null){
            Toast.makeText(this, "Please enter mobile number", Toast.LENGTH_LONG);
            return;
        }
        personViewModel.addPerson(createPerson());
    }
    public void updatePerson(View view){
        if((TextView)findViewById(R.id.mobile) == null){
            Toast.makeText(this, "Please enter mobile number", Toast.LENGTH_LONG);
            return;
        }
        personViewModel.updatePerson(createPerson());
    }
    public void deletePerson(View view){
        if((TextView)findViewById(R.id.mobile) == null){
            Toast.makeText(this, "Please enter mobile number", Toast.LENGTH_LONG);
            return;
        }
        personViewModel.deletePerson(createPerson());
    }
    private Person createPerson(){
        Person p = new Person();
        p.setName(((TextView)findViewById(R.id.name)).getText().toString());
        p.setEmail(((TextView)findViewById(R.id.email)).getText().toString());
        p.setMobile(((TextView)findViewById(R.id.mobile)).getText().toString());

        Address a = new Address();
        a.setLineOne(((TextView)findViewById(R.id.lineOne)).getText().toString());
        a.setCity(((TextView)findViewById(R.id.city)).getText().toString());
        a.setCountry(((TextView)findViewById(R.id.country)).getText().toString());
        a.setZip(((TextView)findViewById(R.id.zip)).getText().toString());

        p.setAddress(a);

        return p;
    }
}
 

ViewModel

 package com.zoftino.room;


import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
import android.arch.lifecycle.Transformations;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;

import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class PersonViewModel extends AndroidViewModel {

    private PersonRepository personRepository = new PersonRepository(this.getApplication());
    private final Executor executor = Executors.newFixedThreadPool(2);

    private final MediatorLiveData<List<Person>> personsByCity = new MediatorLiveData<>();

    private final MediatorLiveData<String> mobileNo = new MediatorLiveData<>();

    //transformation applied so that observer to this LiveData can be added only once
    private final LiveData<Person> personsByMobile = Transformations.switchMap(mobileNo, (mobile) -> {
        return personRepository.getPersonByMobile(mobile);
    });

    public LiveData<List<Person>> getPersonsByCityLive(){
        return personsByCity;
    }
    public PersonViewModel(@NonNull Application application){
        super(application);
    }

    //Room DAO call needs to be run on background thread
    //This example uses Executor
    public void addPerson(Person p){
        executor.execute(() -> {
            personRepository.addPerson(p);
        });
    }
    public void updatePerson(Person p){
        executor.execute(() -> {
        personRepository.updatePerson(p);
        });
    }
    public void deletePerson(Person p){
        executor.execute(() -> {
        personRepository.deletePerson(p);
        });
    }
    //Since room DAO returns LiveData, it runs on background thread.
    public LiveData<List<Person>> getAllPersons(){
        return personRepository.getAllPersons();
    }
    //Room DAO call needs to be run on background thread
    //This example uses AsyncTask
    public void getPersonsByCity(List<String> cities){
        new AsyncTask<Void, Void, List<Person>>() {
            @Override
            protected List<Person> doInBackground(Void... params) {
                return personRepository.getPersonsByCity(cities);
            }
            @Override
            protected void onPostExecute(List<Person> personLst) {
                Log.d("", "persons by city "+personLst.size());
                personsByCity.setValue(personLst);
            }
        }.execute();

    }
    //sets the mobile number to LiveData object,
    //transformation using switchMap intern calls room DAO method which return LiveData
    public void setMobile(String mobile){
        mobileNo.setValue(mobile);
    }
    public LiveData<Person> getPersonsByMobile(){
        return personsByMobile;
    }
}
 

Room Type Converters

Room supports fields of primitive types and primitive object types. If you want to insert other types like date or list in database, you need to define type converters and let room know about them so that it can use them to store the values of types for which type converted are defined.

The methods which convert input type and return target primitive type need to be annotated with TypeConverter. Using TypeConverters annotation on database class, you can list type converters for the target database.

Below is an example type converter that converts List to String.

 public class ListStringConverter {
    @TypeConverter
    public Tags stringToTags(String value) {
        List<String> tagList = Arrays.asList(value.split("\\|"));
        return new Tags(langs);
    }

    @TypeConverter
    public String tagToString(Tags tg) {
        String value = "";

        for (String t :tg.getTagList())
            value += t + "|";

        return value;
    }
}

Informing room about defined type converter.

 @Database(entities = {Info.java}, version = 1)
@TypeConverters({ListStringConverter.class})
public abstract class InfoDatabase extends RoomDatabase {
    public abstract InfoDao infoDao();
} 

Room with RxJava

You can use Room with RxJava so that DAOs can return RxJava observables. There is a separate article on Room with RxJava that you can read to know how to use Room with RxJava.

Room Callbacks

When database is created or opened, you may want to perform some operations in database. Room provides RoomDatabase.Callback abstract class that has onCreate and onOpen callback methods which are called when database is created and each time database is opened respectively.

   RoomDatabase.Callback dbCallback = new RoomDatabase.Callback(){
        public void onCreate (SupportSQLiteDatabase db){
            String SQL_CREATE_TABLE = "CREATE TABLE  log" +
                    "(" +
                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "info TEXT, " +
                    "date  TEXT)";

            db.execSQL(SQL_CREATE_TABLE);
        }
        public void onOpen (SupportSQLiteDatabase db){
            ContentValues contentValues = new ContentValues();
            contentValues.put("info", "db open");
            contentValues.put("date", getDate());

            db.insert("log", OnConflictStrategy.IGNORE, contentValues);
        }
    }; 

Add callback to room database builder as shown below.

                    myDatabase = Room.databaseBuilder(context,
                            MyDatabase.class, "my db").addCallback(dbCallback).build(); 

Room Database Migration

If you want to use existing SQLite database with Room or you want to alter Room database tables, you need to increase database version and add migration class to room database builder. In migration class you need to define db operations which need to be run for migration. For more details, you can read my article on Room database migration.