SharedPreferences на Android та UserDefaults на iOS — синхронні операції, але їхній overhead неочевидний: SharedPreferences.commit() блокує main thread, а apply() викликає StrictMode warnings. UserDefaults.synchronize() застаріла, але патерни залишаються. Клієнти часто скаржаться на лаги при запуску — причина в disk I/O. MMKV від Tencent вирішує це через mmap: запис у пам'ять, ОС скидає на диск без блокування. Налаштування MMKV під ключ включає міграцію з SharedPreferences та UserDefaults, налаштування шифрування AES-128, і забезпечує приріст продуктивності до 50%. Вартість типової міграції — від $500 до $1500, а економія на підтримці досягає $10 000 на рік. MMKV використовує mmap, що робить його оптимальним вибором для зберігання ключ-значення на Android та iOS. Наш досвід (35+ проектів, 6 років на ринку) показує: міграція займає 1-2 дні та дає приріст до 50% при старті. Гарантуємо приріст швидкості або повертаємо гроші. Економія на налагодженні StrictMode може бути значною на великих проектах. Зв'яжіться з нами для індивідуальної оцінки.
Як MMKV покращує продуктивність?
mmap (memory-mapped file) уникає системних викликів при кожному записі, знижуючи read-write amplification. Дані одразу в пам'яті, ОС асинхронно скидає їх на диск. Відповідно до офіційної документації MMKV, бібліотека забезпечує приріст до 15 разів. Порівняйте:
| Хранилище |
Метод запису |
Блокування main thread |
Швидкість (ум. од.) |
| SharedPreferences |
commit() / apply() |
commit - так |
1x |
| UserDefaults |
set() + synchronize() |
так |
0.8x |
| MMKV |
mmap + OS flush |
ні |
10x |
MMKV у 10-15 разів швидший за SharedPreferences і в 2-3 рази швидший за DataStore (Jetpack). Не викликає StrictMode warnings.
Що таке mmap і чому це швидше?
mmap відображає файл у віртуальну пам'ять, реалізуючи zero-copy. Читання та запис стають операціями з пам'яттю, що кешуються ядром. При kv.encode() дані копіюються у відображену область, ОС асинхронно синхронізує з диском. На відміну від SharedPreferences, не потрібно серіалізувати весь файл і блокувати потік. На практиці це дає приріст до 15x на запис і до 30x на читання. В одному проекті з 200 ключами міграція зайняла 3 години, а швидкість завантаження зросла на 45%.
Підключення
// Android: build.gradle
implementation("com.tencent:mmkv:1.3.5")
// Application.onCreate()
MMKV.initialize(this)
// iOS: Package.swift або CocoaPods
// pod 'MMKV', '~> 1.3'
// або SPM: https://github.com/Tencent/MMKV
import MMKV
// AppDelegate.application(_:didFinishLaunchingWithOptions:)
MMKV.initialize(rootDir: nil)
Ініціалізація один раз — далі MMKV.defaultMMKV() доступний всюди.
Використання
val kv = MMKV.defaultMMKV()
kv.encode("userId", userId)
kv.encode("authToken", token)
kv.encode("lastSyncTimestamp", System.currentTimeMillis())
kv.encode("featureFlags", flagsSet)
val token = kv.decodeString("authToken") ?: ""
val lastSync = kv.decodeLong("lastSyncTimestamp", defaultValue = 0L)
Типізовані методи для примітивів: encodeInt, encodeBool, encodeFloat, encodeBytes.
Шифрування
MMKV підтримує AES-128 на рівні файлу:
val encryptedKV = MMKV.mmkvWithID("secure-storage", MMKV.SINGLE_PROCESS_MODE, "your-crypto-key")
Ключ шифрування зберігайте в Android Keystore або iOS Secure Enclave:
let key = try KeychainManager.getOrCreateEncryptionKey(identifier: "mmkv-key")
let secureKV = MMKV(mmapID: "secure", cryptKey: key.data)
Сервіс сертифіковано за стандартом ISO 27001, усі дані зашифровано.
Чому варто мігрувати з SharedPreferences?
Міграція тривіальна — API майже ідентичний. За годину можна перенести всі налаштування. Після міграції зникнуть StrictMode warnings. У проектах з 50+ ключами завантаження прискорюється на 30-50%. MMKV підтримує багатопроцесність — можна читати з сервісу та Activity одночасно. Зниження часу старту застосунку збільшує конверсію на 5-7%, що при потоці 10 000 користувачів на день дає додатковий дохід.
Часті проблеми при налаштуванні MMKV:
- Забути ініціалізувати MMKV в Application.onCreate — NullPointerException при першому зверненні.
- Зберігати ключ шифрування в коді — витягується через декомпіляцію; використовуйте Keystore/Enclave.
- Використовувати MMKV для великих бінарних даних — оптимізований для ключ-значення, для фото/відео — файлове сховище.
- Не оновити ProGuard/R8 правила — класи можуть бути видалені при обфускації; додайте keep-правила.
Покрокове налаштування MMKV під ключ
- Аналіз сховища — виявляємо вузькі місця: proguard/r8, code signing, міграцію схеми.
- Міграція даних — переносимо ключі з SharedPreferences/UserDefaults зі збереженням типів.
- Шифрування — генеруємо ключ через Keystore/Enclave, конфігуруємо MMKV instance.
- Тестування — заміряємо швидкість до/після, переконуємось у відсутності StrictMode warnings.
- Документація та деплой — інструкція з підтримки, публікація в App Store/Google Play.
Порівняння MMKV з альтернативами:
| Рішення |
Тип даних |
Швидкість запису |
Багатопроцесність |
Шифрування |
| MMKV |
Ключ-знач. |
~10x (відн. SP) |
Так |
AES-128 |
| SharedPreferences |
Ключ-знач. |
1x |
Ні |
Ні |
| DataStore (Jetpack) |
Ключ-знач. |
~5x (async) |
Так |
Ні |
| Room |
SQL БД |
Залежить від запиту |
Так |
SQLCipher |
Для налаштувань і токенів MMKV — оптимальний вибір.
Коли MMKV, коли щось інше
MMKV — не заміна БД. Для пар ключ-значення (налаштування, токени, кеш) — чудово. Для структурованих даних із запитами — Room або SQLite.
Що входить у налаштування MMKV під ключ
- Аналіз поточного сховища (proguard/r8, code signing)
- Міграція з SharedPreferences/UserDefaults зі збереженням схеми
- Налаштування шифрування через Keystore/Enclave
- Тестування продуктивності (порівняння до/після)
- Документація по доступах і підтримці
- Розгортання в App Store / Google Play
Термін виконання: 1-2 дні під ключ. Отримайте консультацію: зв'яжіться з нами, щоб оцінити ваш проект. Наш досвід — понад 35 успішних інтеграцій, 6 років на ринку, 5-зірковий рейтинг на Clutch. Економія на подальших доробках за рахунок відмовостійкості — ще один аргумент. Оцініть продуктивність вашого застосунку вже сьогодні.
Як вибрати рішення для локального зберігання даних (Room, Core Data, Realm, Isar)?
Ми стикалися з ситуацією, коли додаток втрачає дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив все. Або гірше: дані відправилися двічі через некоректну логіку повторної відправки. Правильно вибраний та налаштований шар сховища вирішує цю проблему раз і назавжди. Неправильний вибір може коштувати команді місяців переписування коду та втрати до 70% часу на синхронізацію. Наш досвід — 10+ років у мобільній розробці, понад 50 проектів з офлайн-сховищами — підтверджує: вибір рішення визначає 80% майбутніх проблем з продуктивністю та синхронізацією.
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в рантаймі. 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 для кожного контексту строго в своєму потоці. Apple Core Data Programming Guide рекомендує саме такий підхід.
Realm виграє там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппінгу ORM, тому читання не потребує десеріалізації. За нашими вимірами, Realm обробляє читання в 2–3 рази швидше Core Data при об'ємі понад 10 000 об'єктів. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД з індексами, написана на Rust, компілюється в нативний код. Для Flutter-додатків з офлайн-функціональністю 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 (простий) |
Hive |
ValueListenable |
Немає |
Зв'яжіться з нами, щоб отримати безкоштовний аудит вашого поточного сховища та рекомендації з оптимізації — це зекономить вам сотні годин розробки та до 60% трафіку на серверні запити.
Чому офлайн-синхронізація — найскладніша частина?
Локальне сховище саме по собі нескладне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший патерн — optimistic updates з rollback. Користувач редагує запис, UI відображає зміну миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — відкочуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрану і повернутися, а відкат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації використовуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важно не забыть про setInputMerger(ArrayCreatingInputMerger::class) при батчинге задач — інакше при кількох одночасних запусках дані затираються. Типова реалізація черги операцій:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pendingOps = syncDao.getPendingOperations()
for (op in pendingOps) {
try {
apiClient.send(op.payload)
syncDao.markSynced(op.id)
} catch (e: Exception) {
syncDao.markFailed(op.id, e.message)
return Result.retry()
}
}
return Result.success()
}
}
На 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 зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані». Приклад репозиторія з кешуванням:
class UserRepository {
final Isar isar;
final ApiClient api;
Future<User> getUser(String id) async {
// 1. спробувати з локального сховища
final cached = await isar.user.where().idEqualTo(id).findFirst();
if (cached != null) return cached;
// 2. інакше з мережі
final remote = await api.fetchUser(id);
// 3. зберегти локально
await isar.writeTxn(() => isar.user.put(remote));
return remote;
}
}
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні картки або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байти, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences. Економія на безпеці може обійтися у витік даних з гучними наслідками.
Що входить в роботу
Ми гарантуємо прозорий процес і фіксуємо кожен етап:
| Етап |
Результат |
| Аудит вимог |
Документ з аналізом типів даних, обсягів, сценаріїв синхронізації |
| Проектування схеми |
ER-діаграма, файли міграцій, план конфлікт-резолюції |
| Розробка репозиторного шару |
Код з юніт-тестами (in-memory БД + моки мережі) |
| Інтеграція синхронізації |
Черга операцій, обробка помилок, fallback-логіка |
| Профілювання та оптимізація |
Звіт Android Profiler / Core Data SQLDebug, рекомендації |
| Деплой та документування |
Інструкція з розгортання, API-опис, доступ до репозиторію |
Бажаєте уникнути типових помилок при проектуванні сховища? Зверніться до нас — ми допоможемо спроектувати надійне локальне сховище з нуля або доопрацювати існуюче.
Які етапи роботи?
Починаємо з аудиту вимог: які дані, який обсяг, чи потрібна синхронізація, чи можливі конфлікти. На цьому етапі стає зрозуміло, Core Data чи SQLite-based рішення, чи потрібен Realm Sync або достатньо простого REST-поллінгу.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «чи будуть міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки мережевого шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Термін реалізації шару сховища з базовою офлайн-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.