Зберігання даних в 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.RealmModuleclass*-keep class io.realm.internal.Keep-keep @io.realm.internal.Keepclass*{*;}-dontwarn javax.**-dontwarn io.realm.**

Після цього ми можемо починати використовувати Realm.

Використання Realm

Щоб не дублювати документацію Realm, зупинимося лише на деяких моментах, які не зовсім очевидні. У своїх проектах ми використовуємо MVVM-архітектуру, тому й приклади будуть відповідні.

Перш за все, потрібно задати налаштування для Realm. Якщо ми будемо використовувати тільки одну конфігурацію, тоді все, що нам потрібно, це описати конфігурацію за замовчуванням у нашому класі Application:

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

Після цього ми можемо отримати екземпляр Realm таким способом:

Realm realm = Realm.getDefaultInstance();

Є одна важлива деталь. Кожен раз, коли ми викликаємо getDefaultInstance, створюється новий інстанс об’єкта Realm. Тому дуже бажано після використання закривати інстанс за допомогою команди:

realm.close();

У своїх проектах ми зазвичай робимо так: інстанс Realm створюємо і закриваємо в базовому ActivityViewModel:

publicabstractclass ActivityViewModel<A extends BaseActivity, B extends ViewDataBinding>extends BaseObservable {
...
   private Realm realm;
...
   public ActivityViewModel(A activity, B binding){
...
       realm= Realm.getDefaultInstance();
...
   }
 
...
   publicvoid 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), а також геттерів і сеттерів цього поля. Виглядає це так:

publicclass SempleObject extends RealmObject {
 
    privateString name;
 
    @Ignore
    privateString kingName;
 
    // custom setterpublicvoid setKingName(String kingName){ setName("King "+ kingName);}
 
    // custom getterpublicString getKingName(){return getName();}
 
    // setter and getter for 'name'}

Зв’язок багато-до-одного реалізується просто оголосивши поле типу одного з підкласів RealmObject:

publicclass User extends RealmObject {
 
   @PrimaryKey
   privateString id;
 
   @SerializedName("name")privateString name;
 
   @SerializedName("username")privateString username;
 
   @SerializedName("image")privateImage image;
 
   //getters and setters}

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

Зв’язок багато-до-багатьох реалізується за допомогою декларації поля RealmList:

publicclass 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:

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

Уявимо метод, в якому оновлюється наш Conversation:

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

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

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

Якщо у проекті використовується Gson, то програма буде «вилітати» з помилкою java.lang.StackOverflowError. Для того, щоб уникнути цього, потрібно додати даний код при створенні екземпляра Gson:

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

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

publicclass GsonRealmBuilder {
 
   private GsonRealmBuilder(){}
 
   publicstatic GsonBuilder getBuilder(){returnnew GsonBuilder()
               .setExclusionStrategies(new ExclusionStrategy(){
                   @Override
                   publicboolean shouldSkipField(FieldAttributes f){return f.getDeclaringClass().equals(RealmObject.class);}
 
                   @Override
                   publicboolean shouldSkipClass(Class<?> clazz){returnfalse;}});}
 
   publicstatic Gson get(){return getBuilder().create();}}

Висновки

Загалом, у нас переважно позитивні враження від використання Realm. Тепер впровадження баз даних займає значно менше часу, ніж з SQLite. Звичайно, є деякі моменти, які хотілось би покращити, але це вже інша історія.

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