Write a post
Published Mar 06, 2017

Replace SQLite with Realm Database in Android

Replace SQLite with Realm Database in Android

What is Realm Database

According to the official Realm description, Realm database is a mobile first database built from the ground-up to run directly inside phones, tablets, and wearable. Realm is a "better database: faster, simpler, and Java-native." The words "better, faster, and simpler" imply a comparison — the comparison here is between Relam and SQLite for Android. While SQLite will continue to have its place in Android development, I believe that Realm database saves developers a lot of time and most new Android development projects should use Realm for data persistence instead of SQLite to increase developer productivity.

How does Realm Database Work?

Realm database works by saving Java objects directly to disk as objects instead of first mapping them to another data type like SQLite does. At the center of Realm database is this thing called Realm, which is equivalent to a traditional database. Realms can map different kinds of objects to one file on disk. Another way to look at it is that Realms are databases that do not require a separate mapping from Java objects to the persisted version on the disk.

It is kind of like a what-you-see-is-what-is-saved workflow — changes to the object in the user interface are automatically saved to the database if the object is a Realm managed object. Realm managed objects are your equivalent of SQLite tables. For Java object to become Realm managed, the class must either extend RealmObject or implement RealmModel interface.

Demo App

To understand Realm database, let us create a simple demo app that demonstrates the core concepts of Realm database. This will be a simple quote app that displays a list of motivational quotes from different authors. Here is a screenshot of what the app will look like.
quote_list_green_framed_resized.png

Setup Realm Database

The productivity gain from replacing SQLite with Realm database starts with some simple steps. To add Realm to a new or an existing Android project requires the following steps.

First, you add the following class path dependency to the project level build.gradle file like this:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath "io.realm:realm-gradle-plugin:2.3.1"
  }
}

Then, you apply the realm-android plugin to the top of application level build.gradle file like this:

apply plugin: 'com.android.application'
apply plugin: 'realm-android'

After that you can go ahead and resynch gradle. After you have added Realm to your project, the next step is to initialize it. The best place to perform this initialization is in a custom Application class. For our demo project, I have added an Application class called ProntoQuoteApplication.java and here is the content of the class that shows how Realm database is initialized.

public class ProntoQuoteApplication extends Application {
 
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)){
            return;
        }
        initRealm();
    }
 
    private void initRealm() {
        Realm.init(this);
        RealmConfiguration config = new RealmConfiguration.Builder()
                .name("prontoschool.realm")
                .schemaVersion(1)
                .deleteRealmIfMigrationNeeded()
                .build();
        Realm.setDefaultConfiguration(config);
}

In the above code, I used RealmConfiguration object to pass some parameters to Realm, such as the name we want to give to our Realm database. This step is optional — if we did not provide a configuration object, then the default configuration will be used instead. If you want to to build your own configuration, you must remember to set it as the default configuration.

Create Realm Database Table

There really is no such thing as Realm database tables; instead, we have Realm managed objects. The phrase “create tables” is used because of its familiarity. In order to create tables or auto-updating objects, all we need to do is to make sure that our model classes all extend from RealmObject base class. While we're doing that, we can take advantage of Realm features, (such as easy handling of Relationships) to define one-to-many relationships (such as the relationship between an Author and Quote) in our app. One Author can have many Quotes but one Quote can only have one Author:

  public class Quote extends RealmObject{
      @PrimaryKey
      private long id;
      private String quote;
      private Author author;
      private Category category;
      private String quoteBackgroundImageUrl;
      private boolean isFavourite;
  }

  public class Author extends RealmObject{
      @PrimaryKey
      private long id;
      private String authorName;
      private String authorImageUrl;
      private RealmList<Quote> quotes;
    }

And that is all, we can now save instances of these classes directly to Realm database.

Auto Increment of Realm Database Primary Key

Currently, Realm does not support auto increment of primary key. You can work arround this limitation by manually incrementing the primary keys yourself. How I handle this is that I define a static variable of type AtomicLong in the custom Application class.

Each time the app launches, I will check to see if the table, or rather, the managed object has been created. If it has, then I will get the max primary key of that object and save it in the AtomicLong variable. Then each time I want to save an object to the database, I will get that primary key and increment it so that it will be ready for the next use. Here is how this is implemented in code:

public class ProntoQuoteApplication extends Application {

    public static AtomicLong quotePrimaryKey;
    public static AtomicLong authorPrimaryKey;

    @Override
    public void onCreate() {
        super.onCreate();
        initRealm();        
    }    

    private void initRealm() {
        Realm.init(this);
        RealmConfiguration configuration  = new RealmConfiguration.Builder()
                .name(Constants.REALM_DATABASE)
                .schemaVersion(1)
                .deleteRealmIfMigrationNeeded()
                .build();
        //Now set this config as the default config for your app
        //This way you can call Realm.getDefaultInstance elsewhere

        Realm.setDefaultConfiguration(configuration);

        //Get the instance of this Realm that you just instantiated
        //And use it to get the Primary Key for the Quote and Category Tables
        Realm realm = Realm.getInstance(configuration);

        try {
            //Attempt to get the last id of the last entry in the Quote class and use that as the
            //Starting point of your primary key. If your Quote table is not created yet, then this
            //attempt will fail, and then in the catch clause you want to create a table
            quotePrimaryKey = new AtomicLong(realm.where(Quote.class).max("id").longValue() + 1);
        } catch (Exception e) {
            //All write transaction should happen within a transaction, this code block
            //Should only be called the first time your app runs
            realm.beginTransaction();

            //Create temp Quote so as to create the table
            Quote quote = realm.createObject(Quote.class, 0);

            //Now set the primary key again
            quotePrimaryKey = new AtomicLong(realm.where(Quote.class).max("id").longValue() + 1);

            //remove temp quote
            RealmResults<Quote> results = realm.where(Quote.class).equalTo("id", 0).findAll();
            results.deleteAllFromRealm();
            realm.commitTransaction();
        }

        try {
            //Attempt to get the last id of the last entry in the Author class and use that as the
            //Starting point of your primary key. If your Author table is not created yet, then this
            //attempt will fail, and then in the catch clause you want to create a table
            authorPrimaryKey = new AtomicLong(realm.where(Author.class).max("id").longValue() + 1);
        } catch (Exception e) {
            //All write transaction should happen within a transaction, this code block
            //Should only be called the first time your app runs
            realm.beginTransaction();

            //Create temp Author so as to create the table
            Author author = realm.createObject(Author.class, 0);

            //Now set the primary key again
            authorPrimaryKey = new AtomicLong(realm.where(Author.class).max("id").longValue() + 1);

            //remove temp author
            RealmResults<Author> results = realm.where(Author.class).equalTo("id", 0).findAll();
            results.deleteAllFromRealm();
            realm.commitTransaction();
        }        
    }
}

Opening and Closing Realm Instances

With SQLite, you call getWritableDatabase() or getReadableDatabase() against a SQLiteOpenHelper class to obtain an instance of SQLiteDatabase. With Realm, you call Realm.getDefaultInstance() to get an instance of Realm. Just like how it is recommended to close SQLite database when you are done using it, it is also highly recommended to close Realm instances when you are finished. This way, \the allocated memory to that instance will be reallocated.

@Override
    public void onDestroy() {
        if (!realm.isClosed()) {
            realm.close();
        }
        super.onDestroy();
    }

How to Add Data to Realm Database

To add data to Realm database, you can create a Realm managed object instance of that class. This must be enclosed in a transaction to be effective. For example, here is how to save a Quote object to the database. Realm write apis with asynchronous version that you can use to perform your database operation in a background thread.

public void addAsync(final Quote quote,  final String authorName) {
        final Realm insertRealm = Realm.getDefaultInstance();
        final long id = ProntoQuoteApplication.quotePrimaryKey.getAndIncrement();
        insertRealm.executeTransactionAsync(new Realm.Transaction() {
            @Override
            public void execute(Realm backgroundRealm) {
                final Author author = createOrGetAuthor(authorName, backgroundRealm);
                Quote savedQuote = backgroundRealm.createObject(Quote.class, id);
                savedQuote.setQuote(quote.getQuote());
                savedQuote.setAuthor(author);
                savedQuote.setQuoteBackgroundImageUrl(quote.getQuoteBackgroundImageUrl());
                savedQuote.setFavourite(quote.isFavourite());
            }
        }
   }

Query for Data from Realm Database

Querying for data in Realm is fast and easy — that is where a lot of the productivity gains come from. Realm’s query engine uses a Fluent interface to construct multi-clause queries. You can construct your query inline or use RealmQuery object to construct your query. Realm query results are returned as a RealmResult<T>, where T is a Realm managed class. The Realm objects that are contained in RealmResult are live objects. You are free to read the values where you see fit, but if you want to update the value in any object you must perform that in a transaction.

Here is a sample query that returns all the quotes in the database.

public List<Quote> getAllQuotes(Realm passedInRealm) {
        RealmResults<Quote> result = passedInRealm.where(Quote.class).findAll();
        return result;
    }

Working with Realm in an MVP Architecture

One of the most powerful features of Realm database is the live, auto-updating object. This means that changes to object are immediately persisted to disk and almost instantly propagated to any listener of that object. The challenge with taking advantage of this feature is that you will not be able to apply seperation of concerns pattern, such as MVP.

The reason for this is that each update to a Realm managed object should be wrapped in transaction. This means that you will be accessing the database in your Activity, Fragment, or any other view where you recieve user inputs.

To structure your Realm backed Android Studio project with MVP design pattern, you have to handle the updates yourself. What I do is that I create an interface that I then have my Presenters implement, and this interface is used to surface the database results to the view. If you are not familiar with MVP Design pattern in Android, this tutorial might help you. Here is the interface

public interface OnDatabaseOperationCompleteListener {
    void onSaveOperationFailed(String error);
    void onSaveOperationSucceeded(long id);
    void onDeleteOperationCompleted(String message);
    void onDeleteOperationFailed(String error);
    void onUpdateOperationCompleted(String message);
    void onUpdateOperationFailed(String error);
}

With that interface, here is how I save an Author to the database in a Repository class. The good thing with this pattern is that my Activity and Fragments are not aware of the database implementation that I am using.

public void saveAsync(final Author author, final OnDatabaseOperationCompleteListener listener) {
        final Realm insertRealm = Realm.getDefaultInstance();
        final long id = ProntoQuoteApplication.authorPrimaryKey.incrementAndGet();
        insertRealm.executeTransactionAsync(new Realm.Transaction() {
            @Override
            public void execute(Realm backgroundRealm) {
                Author author1 = backgroundRealm.createObject(Author.class, id);
                author1.updateToRealm(author);
            }
        }, new Realm.Transaction.OnSuccess() {
            @Override
            public void onSuccess() {
                insertRealm.close();
                listener.onSaveOperationSucceeded(id);
            }
        }, new Realm.Transaction.OnError() [{]()
            @Override
            public void onError(Throwable error) {
                insertRealm.close();
                listener.onSaveOperationFailed(error.getMessage());
            }
        });

    }

The complete demo application can be found here. And if you have questions I will be glad to help.

Discover and read more posts from Val Okafor
get started
Enjoy this post?

Leave a like and comment for Val

6