Зберігання даних в 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 або прототип додатка? Ознайомтеся з нашим портфоліо і зробіть замовлення вже сьогодні!