Локальне сховище в мобільних додатках: Core Data, Room, Realm, Hive, Isar
Додаток теряє дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив усе. Або гірше: дані були відправлені двічі через неправильну логіку повторної відправки. Правильно вибране та налаштоване сховище вирішує цю проблему раз і назавжди.
Вибір бази даних — не питання смаку
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в runtime. Room добре інтегрується з Kotlin Flow та LiveData, що робить реактивні UI-оновлення прямолінійними. Основна складність — міграції схеми. @Database(version = N, exportSchema = true) з файлами міграцій в assets/databases/ — обов'язкова практика, інакше при оновленні додатку fallbackToDestructiveMigration() просто сотре дані користувача.
Core Data (iOS) — не база даних, а фреймворк управління графом об'єктів поверх SQLite (або XML, або in-memory). NSPersistentContainer з viewContext для читання на main thread та newBackgroundContext() для запису — базова схема. Проблема починається, коли розробник робить save() в viewContext з фонового потоку: EXC_BAD_ACCESS в випадковий момент, відтворюється раз на тиждень, в краш-логу мало корисного. Необхідно використовувати performAndWait або perform для кожного контексту строго у своєму потоці.
Realm перемагає там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппингу ORM, тому читання не потребує десеріалізації. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД, написана на Rust, компілюється в нативний код. Для Flutter-додатків з offline-функціональністю Isar зараз краще: вбудований query builder з типобезпечними фільтрами, трансакції, watchObject/watchQuery для реактивності.
| Платформа | Рішення | Реактивність | Синхронізація |
|---|---|---|---|
| Android | Room + Flow | LiveData/Flow | WorkManager |
| iOS | Core Data | NSFetchedResultsController | CloudKit |
| Flutter | Isar | Streams | Custom / Realm Sync |
| Cross-platform | Realm | RealmResults.observe | Device Sync |
| Flutter (simple) | Hive | ValueListenable | Немає |
Offline-синхронізація — тут починається справжня складність
Локальне сховище саме по собі несклідне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший паттерн — optimistic updates з rollback. Користувач редагує запис, UI відображає змінення миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — откатуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрана та повернутися, а откат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації користуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важливо не забути про setInputMerger(ArrayCreatingInputMerger::class) при батчингу задач — інакше при кількох одночасних запусках дані затираються.
На iOS аналог — BGTaskScheduler з BGProcessingTaskRequest. Обмеження iOS на фонове час виконання (~30 секунд для refresh tasks) означають, що синхронізація повинна бути інкрементальною: не «синхронізувати всё», а «синхронізувати наступні N записів, зберегти курсор».
Конфлікти при мультипристрійній роботі вирішуються одним з трьох підходів:
- Last-write-wins по
updated_at(найпростіший, теряє дані при одночасному редагуванні) - Server-wins (клієнт завжди приймає серверну версію)
- Three-way merge (складно, потрібен спільний предок — підходить для документів)
У більшості B2C-додатків достатньо last-write-wins з вектором часу на рівні користувача, але при спільному редагуванні потрібен CRDTs-підхід — тоді дивимося на Automerge або Yjs з мобільними біндингами.
Як будуємо шар сховища
Репозиторний паттерн — не опціональний, а обов'язковий. UserRepository не знає, звідки дані: з Room, Realm або мережі. ViewModel викликає repository.getUser(id), отримує Flow/Stream, відображає дані. Логіка кеширування — всередині репозиторію.
Для Flutter типова архітектура: Isar для персистентності, Riverpod для управління станом, ConnectivityPlus для визначення стану мережі, кастомний SyncService з чергою операцій. Riverpod AsyncNotifier зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані».
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні карти або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байта, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences.
Етапи роботи
Починаємо з аудиту вимог: які дані, який обсяг, потрібна ли синхронізація, можливі ли конфлікти. На цьому етапі стає ясно: Core Data або SQLite-based рішення, потрібен ли Realm Sync або хватить простого REST-поллінга.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «будуть ли міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки сітьового шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Терміни для реалізації шару сховища з базовою offline-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції.







