Хранение данных в Android с помощью Realm

Зберігання даних в Android за допомогою Realm

Существует три способа сохранения данных мобильного приложения: Shared preferences/User defaults, файлы и база данных. Выбор того или иного способа зависит от объема данных, с которыми имеет дело приложение, их типа и того, что нужно с этими данными делать. Традиционно разработчики мобильных приложений использовали SQLite, но существует еще один подход – Realm, мобильная база данных, о которой мы сегодня и поговорим.

Сначала для длительного хранения данных фактически каждое мобильное приложение использовало систему SQLite — либо непосредственно, либо через одну из многих библиотек, обеспечивающих удобство обертки вокруг нее.

Технології баз даних 2000-2014

Из изображения выше видно, что количество серверных технологий для баз данных (синий цвет) было выпущено гораздо больше, чем для мобильных (красный). В 2000 году система SQLite была революционным решением, однако очевидно, что за 16 лет разработка мобильных приложений существенно изменилась.

За это время было создано множество библиотек-оберток для SQLite, позволяющих упростить использование CRUD-операций (создание, чтение, обновление, удаление). Примеры таких библиотек: SugarORM, GreenDAO, Core Data, ORMLite и т.д. В 2014 году появилась совершенно новая альтернатива использованию реляционных баз данных – Realm.

Преимущества Realm

Одним из наибольших преимуществ использования Realm является скорость работы. Если верить информации на официальной странице Realm, то результаты обработки запроса в базу данных из 200K записей будут следующими:

Швидкість роботи Realm (Counts)

При перебирании всех записей, соответствующих запросу:

Швидкість роботи Realm (Queries)

При вставке 200K записей в одной транзакции:

Швидкість роботи Realm (Inserts)

Достаточно неплохой результат, по-моему.

Еще одна интересная особенность, которую мы получаем из Realm, это денормализация — хранение имеющих связей моделей на месте.

Предположим, что у нас есть две модели: пользователь и адрес электронной почты. Пользователь может иметь несколько почтовых ящиков. Каждый раз, когда нам нужна модель пользователя, нам также доступны адреса всех его электронных ящиков. Нам не нужно выполнять дополнительные запросы на список всех его адресов. Мы можем сохранить адреса его почтовых ящиков в виде списка моделей в пользовательской модели.

И третье, не менее весомое преимущество Realm – это простота использования. В этом мы убедимся на примерах.

Подключение Realm

Предпосылки использования:

  • Нет поддержки Java вне Android
  • Android Studio >= 1.5.1
  • JDK version => 7
  • Поддерживаются все устройства работающие на Android-версии выше 9-го API (Android 2.3 Gingerbread и выше)

Realm устанавливается как Gradle-плагин:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:0.88.1"
    }
}
 
apply plugin: 'realm-android'

Системы сборки Maven и Ant не поддерживаются. Начиная с версии 0.88, поддержка Eclipse считается устаревшей. Если вы хотите продолжать использовать Eclipse, вам придется работать с Realm v0.87.5, но обратите внимание, что эта версия не получит никаких дополнительных обновлений.

При компиляции Realm генерирует прокси-класс для каждого RealmObject. Для того чтобы гарантировать, что эти классы можно будет найти после запуска обфускации и статического анализа, нужно добавить конфигурацию ниже в файл конфигурации ProGuard:

-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-keep class io.realm.internal.Keep
-keep @io.realm.internal.Keep class * { *; }
-dontwarn javax.**
-dontwarn io.realm.**

После этого мы можем начинать использовать Realm.

Использование Realm

Чтобы не дублировать документацию Realm, остановимся лишь на некоторых моментах, которые не совсем очевидны. В своих проектах мы используем архитектуру MVVM, поэтому и примеры будут соответствующие.

Прежде всего нужно задать настройки для Realm. Если мы будем использовать только одну конфигурацию, тогда все, что нам нужно это описать конфигурацию по умолчанию в нашем классе Application:

@Override
public void onCreate() {
   super.onCreate();
...
   RealmConfiguration config = new RealmConfiguration.Builder(this).build();
   Realm.setDefaultConfiguration(config);
	...
}

После этого мы можем получить экземпляр Realm следующим способом:

Realm realm = Realm.getDefaultInstance();

Есть одна важная деталь. Каждый раз, когда мы вызываем getDefaultInstance, создается новый инстанс объекта Realm. Поэтому очень желательно после использования закрывать инстанс с помощью команды:

realm.close();

В своих проектах мы обычно делаем так: инстанс Realm создаем и закрываем в базовом ActivityViewModel:

public abstract class ActivityViewModel<A extends BaseActivity, B extends ViewDataBinding>
       extends BaseObservable {
...
   private Realm realm;
...
   public ActivityViewModel(A activity, B binding) {
...
       realm = Realm.getDefaultInstance();
...
   }
 
...
   public void onDestroy() {
       realm.close();
   }
 
   public Realm getRealm() {
       return realm;
   }
...
}

Это позволяет нам получать экземпляр Realm в любом месте наших ViewModel и не волноваться об утечке памяти.

Для асинхронных задач и отдельных потоков надежным паттерном является:

    Realm realm = null;
    try {
        realm = Realm.getDefaultInstance();
 
        // ... Use the Realm instance
    } finally {
        if (realm != null) {
            realm.close();
        }
    }

Для того чтобы создать модель, нам нужен обычный POJO, который наследуется от RealmObject. Но есть некоторые ограничения. К примеру, нужно обязательно декларировать гетеры и сеттеры в своих моделях. Но если ваш сеттер или геттер выполняет еще какие-либо дополнительные действия, эти действия будут игнорироваться. Обходной путь заключается в использовании игнорируемых полей (аннотация @Ignore), а также геттеров и сеттеров этого поля. Выглядит это так:

public class SempleObject extends RealmObject {
 
    private String name;
 
    @Ignore
    private String kingName;
 
    // custom setter
    public void setKingName(String kingName) { setName("King " + kingName); }
 
    // custom getter
    public String getKingName() { return getName(); }
 
    // setter and getter for 'name'
}

Связь многое к одному реализуется просто объявив поле типа одного из подклассов RealmObject:

public class User extends RealmObject {
 
   @PrimaryKey
   private String id;
 
   @SerializedName("name")
   private String name;
 
   @SerializedName("username")
   private String username;
 
   @SerializedName("image")
   private Image image;
 
   //getters and setters
}

Каждый пользователь (экземпляр User) имеет либо 0, либо 1-е изображение (экземпляр Image). В Realm ничто не помешает вам использовать один и тот же объект изображения для нескольких пользователей, и модель выше может иметь связь много к другому, но чаще используется для моделирования связей друг к другу.

>

Связь многих реализуется с помощью декларации поля RealmList:

public class User extends RealmObject {
 
   ...
 
   private RealmList<Image> images;
 
   //getters and setters
}

Realm будет автоматически обновляться до последней версии при каждом изменении в базе данных. Это удобная функция, позволяющая сохранять UI с последней версией данных прилагая минимум усилий.

Если автоматического обновления недостаточно, мы можем получать callback при любых изменениях в нашей выборке. Например:

RealmResults<Message> results = getRealm()
.where(Message.class)
.equalTo("conversation.id", conversationId);
 
results.addChangeListener(() -> {
//handle callback
});

Недостатком такого варианта является то, что мы не знаем, в каком именно элементе произошли изменения. В этом случае мы используем EventBus. Важно помнить, что Realm-объект, созданный в одном потоке, мы не можем использовать в другом. Поэтому в событиях мы передаем не сам Realm-объект, а только его id.

Декларируем событие обновления Conversation:

public class ConversationEvent {
   public String conversationId;
 
   public ConversationEvent(String conversationId) {
       this.conversationId = conversationId;
   }
}

Представим метод, в котором обновляется наш Conversation:

public void updateConversation(Conversation conversation) {
	...
   EventBus.getDefault().post(new ConversationEvent(conversation.getId()));
}

Так будет выглядеть наш ActivityViewModel:

public class ConversationActivityVM
       extends ActivityViewModel<ConversationActivity, ActivityConversationBinding> {
...
 
//Подписываемся на события ConversationEvent
@Subscribe(threadMode = ThreadMode.MAIN)
public void onConversationEvent(ConversationEvent event) {
   if (adapter != null) {
Conversation conversation = realm.where(Conversation.class).equalTo("id", id).findFirst();
       adapter.addOrUpdate(conversation);
   }
}
 
   @Override
   public void onStart() {
       EventBus.getDefault().register(this);
   }
 
   @Override
   public void onStop() {
       EventBus.getDefault().unregister(this);
   }
 
   ...
}

Если в проекте используется Gson, то приложение будет «вылетать» с ошибкой java.lang.StackOverflowError. Для того чтобы избежать этого, нужно добавить данный код при создании экземпляра Gson:

Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }
 
            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .create();

Для удобства мы создали билдер и пользуемся им для получения экземпляра класса Gson:

public class GsonRealmBuilder {
 
   private GsonRealmBuilder() {
   }
 
   public static GsonBuilder getBuilder() {
       return new GsonBuilder()
               .setExclusionStrategies(new ExclusionStrategy() {
                   @Override
                   public boolean shouldSkipField(FieldAttributes f) {
                       return f.getDeclaringClass().equals(RealmObject.class);
                   }
 
                   @Override
                   public boolean shouldSkipClass(Class<?> clazz) {
                       return false;
                   }
               });
   }
 
   public static Gson get() {
       return getBuilder().create();
   }
}

Выводы

В общем, у нас преимущественно положительные впечатления от использования Realm. Теперь внедрение баз данных занимает гораздо меньше времени, чем с SQLite. Конечно, есть некоторые моменты, которые хотелось бы улучшить, но это уже другая история.

Нужен MVP, разработка под iOS, Android или прототип приложения? Ознакомьтесь с нашим портфолио и сделайте заказ уже сегодня!