Інтеграція Firebase Realtime Database в мобільний додаток
Firebase Realtime Database (RTDB) — JSON-дерево з WebSocket-синхронізацією. Це не реляційна БД і не документна БД у класичному сенсі. Ключова фіча — офлайн-персистентність і real-time синхронізація з коробки. Але неправильна структура даних у RTDB перетворює ці переваги на проблему: підписка на вузол з глибоким вкладенням затягне все піддерево в пам'ять пристрою. Ми стикалися з проєктами, де через плоску структуру додаток вивантажував 50 МБ даних при кожному оновленні. Для real-time чатів RTDB у 2-3 рази швидше Firestore за затримкою доставки повідомлень. Як цього уникнути — розберемо нижче.
Чому неправильна структура даних вбиває продуктивність?
RTDB не підтримує JOIN і не вміє вибирати підмножину вузла. Якщо вкласти пости в профіль користувача, то при читанні users/$uid ви отримаєте всі пости цілком. При офлайн-персистентності вони кешуються, займаючи місце. Рішення — денормалізація та зворотні індекси. Приклад коректної структури:
{
"users": { "uid123": { "name": "Іван", "email": "ivan@..." } },
"posts": { "postId1": { "userId": "uid123", "text": "...", "createdAt": 1700000000 } },
"userPosts": { "uid123": { "postId1": true, "postId2": true } }
}
userPosts — зворотний індекс для отримання постів конкретного користувача без сканування всього вузла posts. Це стандартний патерн для RTDB. Ми гарантуємо, що структура буде спроєктована з урахуванням патернів Firebase — понад 10 проєктів інтеграції.
Як ми інтегруємо Firebase RTDB?
Процес роботи включає етапи, кожен з яких супроводжується Code Review і тестуванням на реальних пристроях. Досвід нашої команди — понад 20 проєктів з Firebase.
| Етап |
Опис |
Орієнтовний термін |
| Аналіз сценаріїв |
Визначаємо, які дані потребують real-time |
від 2 днів |
| Проектування структури |
Денормалізація, зворотні індекси, шардування |
від 3 днів |
| Налаштування правил безпеки |
Валідація, аутентифікація, обмеження доступу |
від 1 дня |
| Реалізація підписок |
Офлайн-персистентність, вибір on('value') vs on('child_added') |
від 4 днів |
| Оптимізація кешу |
keepSynced, розмір кешу |
від 1 дня |
| Навантажувальне тестування |
Перевірка до 10k concurrent users |
від 2 днів |
| Документація та передача |
Архітектурна схема, опис правил, деплой |
від 1 дня |
Вартість розраховується індивідуально після аналізу вашого проєкту. Зв'яжіться з нами для консультації.
Детальніше про транзакції
Лайки, лічильники, баланс — будь-який конкурентний інкремент потребує транзакцій:
const likeRef = database().ref(`/posts/${postId}/likes`);
await likeRef.transaction(currentLikes => (currentLikes ?? 0) + 1);
transaction() атомарно читає та записує. Якщо між read і write інший клієнт змінив значення — транзакція повторюється автоматично (до 25 разів). Для лайків це єдино правильний підхід — set(currentLikes + 1) дасть race condition при одночасних натисканнях.
Як уникнути витоків пам'яті при підписках?
import database from '@react-native-firebase/database';
useEffect(() => {
const ref = database().ref(`/userPosts/${userId}`);
const onValue = ref.on('value', snapshot => {
const postIds = Object.keys(snapshot.val() ?? {});
setPostIds(postIds);
});
const onChildAdded = ref.on('child_added', snapshot => {
setPostIds(prev => [...prev, snapshot.key!]);
});
return () => {
ref.off('value', onValue);
ref.off('child_added', onChildAdded);
};
}, [userId]);
Критично: завжди викликайте ref.off() при демонтуванні. on() без off() — memory leak: слухач живе вічно, перерендерює компонент, який уже видалено. У продакшні це крэш з Can't perform a React state update on an unmounted component. Альтернативно — абстрагуйте слухачі в користувацький хук з автоматичним знищенням.
Офлайн-персистентність
import database from '@react-native-firebase/database';
database().setPersistenceEnabled(true);
database().setPersistenceCacheSizeBytes(10 * 1024 * 1024); // 10 MB
setPersistenceEnabled(true) вмикає SQLite-кеш на пристрої. При офлайні додаток читає з кешу. При відновленні мережі — синхронізує зміни. Викликати лише один раз при ініціалізації, до першого підключення до БД.
keepSynced(true) на конкретному вузлі — попередньо завантажує дані та тримає в кеші, навіть якщо немає активних слухачів. Обережно: не застосовуйте до великих вузлів — RTDB скачає все дерево.
Налаштування правил безпеки
Згідно з Firebase Realtime Database Security Rules Guide, дефолтні правила RTDB — або всім читати/писати, або нікому. Обов'язково налаштовуємо перед продакшном:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"posts": {
"$postId": {
".read": "auth != null",
".write": "auth != null && newData.child('userId').val() === auth.uid",
".validate": "newData.hasChildren(['userId', 'text', 'createdAt'])"
}
}
}
}
.validate — перевіряє структуру даних перед записом. Без валідації клієнт може записати довільний JSON.
Вибір між RTDB та Firestore
| Критерій |
RTDB |
Firestore |
| Тип даних |
Ієрархічні (JSON) |
Документно-орієнтовані |
| Запити |
Тільки за ключами та фільтрація |
Складні запити, складені індекси |
| Масштабування |
До 1M concurrent |
Вище, автомасштабування |
| Real-time |
Низька затримка (WebSocket) |
Висока затримка, але ширший функціонал |
| Офлайн |
Персистентність включена завжди |
Опціонально |
| Ціна |
$5/ГБ сховища + $1/ГБ трафіку |
$0.06/100K операцій |
Висновок: RTDB — для чатів, присутності, ігор. Firestore — для складних запитів і великої кількості користувачів. Наша команда допомагає вибрати оптимальний варіант.
Що входить в роботу по інтеграції Firebase RTDB
- Архітектурна схема даних: денормалізація, зворотні індекси, шардування.
- Реалізація підписок з офлайн-персистентністю: код для React Native / Flutter / iOS / Android.
- Налаштування правил безпеки: валідація, аутентифікація, оптимізація.
- Навантажувальне тестування: перевірка до 10k concurrent users.
- Документація: опис API, правил, інструкція по деплою.
- Підтримка після запуску: гарантія 3 місяці.
Оцінимо ваш проєкт безкоштовно. Замовте інтеграцію Firebase RTDB вже сьогодні.
Як вибрати рішення для локального зберігання даних (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 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.