Ми часто стикаємося з ситуацією: додаток на Android гальмує при масовому вставленні даних — запис 1000 об'єктів через Room займає 15 секунд. Клієнт втрачає користувачів через лаги. ObjectBox вирішує цю проблему: нативна C++ реалізація без SQL-парсингу дає до 10x прискорення на операціях запису. У нашій практиці ми впровадили ObjectBox у IoT-трекер, де кожні 5 хвилин зберігалося 500 точок — час запису впав з 8 до 0.7 секунд. Економія часу розробки: до 2 тижнів на проекті за рахунок автоматичного codegen і відсутності DAO. Додатково об'єктна модель скорочує обсяг коду на 30-40%, що прямо знижує витрати на розробку та підтримку.
Ми налаштовуємо ObjectBox під ключ, включаючи моделі, реактивні запити та синхронізацію. Оцінимо ваш проект за один день — зв'яжіться з нами, щоб почати.
Чому ObjectBox швидший за SQLite?
ObjectBox — об'єктна база, не реляційна. Немає JOIN, немає GROUP BY — але для роботи з графом об'єктів (сутності зі зв'язками) це не потрібно. Бенчмарки ObjectBox показують 10x перевагу перед Room/SQLite на записі. У реальних проектах розрив залежить від патернів: масові вставки — виграш 3–10x, читання за ID — приблизно однаково. Для IoT-додатків, трекерів, ігрових збережень, каталогів з фільтрацією ObjectBox ідеальний. Економія на розробці — до 40% порівняно з традиційними рішеннями.
Як налаштувати ObjectBox за 3–5 днів?
Процес налаштування включає кілька кроків:
- Аналітика схеми даних: виявляємо сутності та зв'язки.
- Проектування Entity з відношеннями (ToOne, ToMany).
- Інтеграція BoxStore в Application клас.
- Реалізація реактивних запитів через DataObserver.
- Тестування продуктивності: заміри швидкості запису/читання.
- Деплой у сторі додатків.
Нижче — типовий стек і код.
Підключення та моделі
// build.gradle (app)
plugins {
id("io.objectbox")
}
dependencies {
implementation("io.objectbox:objectbox-kotlin:3.8.0")
}
@Entity
data class Task(
@Id var id: Long = 0,
var title: String = "",
var description: String = "",
var priority: Int = 0,
var dueDate: Long = 0,
var isCompleted: Boolean = false
) {
val tags: ToMany<Tag> = toMany()
}
@Entity
data class Tag(
@Id var id: Long = 0,
var name: String = "",
@Index var color: String = ""
)
@Id — обов'язковий Long для внутрішнього ObjectBox ID. Це не UUID і не ваш бізнес-ідентифікатор — використовуйте окреме строкове поле для синхронізації з сервером.
Ініціалізація та BoxStore
// Application клас
class MyApp : Application() {
companion object {
lateinit var boxStore: BoxStore
private set
}
override fun onCreate() {
super.onCreate()
boxStore = MyObjectBox.builder()
.androidContext(this)
.name("tasks-db")
.build()
}
}
// Використання в ViewModel
class TaskViewModel : ViewModel() {
private val taskBox: Box<Task> = MyApp.boxStore.boxFor(Task::class.java)
val tasks: LiveData<List<Task>> = liveData(Dispatchers.IO) {
val query = taskBox.query(Task_.isCompleted.equal(false))
.order(Task_.priority, QueryBuilder.DESCENDING)
.build()
emitSource(query.subscribe().toLiveData())
}
}
query.subscribe().toLiveData() — ObjectBox DataObserver автоматично сповіщає при зміні даних у box. Це реактивна підписка без зайвого коду.
Запити через QueryBuilder
ObjectBox не використовує строковий SQL. Запити через типобезпечний QueryBuilder з Properties — згенерованими метакласами (Task_, Tag_):
// Фільтрація з декількома умовами
val urgentTasks = taskBox.query(
Task_.isCompleted.equal(false)
.and(Task_.priority.greater(2))
.and(Task_.dueDate.less(System.currentTimeMillis() + 86_400_000L))
)
.order(Task_.dueDate)
.build()
.find()
// Повнотекстовий пошук — потребує @Index(type = IndexType.VALUE) + FTS
val searchResults = taskBox.query(
Task_.title.contains("meeting", StringOrder.CASE_INSENSITIVE)
)
.build()
.find()
Немає JOIN — відношення через ToOne/ToMany. ObjectBox підвантажує пов'язані об'єкти lazy за замовчуванням:
// ToMany завантажується при першому зверненні
val taskTags = task.tags // lazy load, звернення до DB
ObjectBox Sync
Зазначимо: як і Realm, ObjectBox пропонує комерційний сервер синхронізації — ObjectBox Sync. Двостороння синхронізація, конфлікт-резолюція, дельта-оновлення. Це окремий продукт з ліцензійною моделлю. Ми інтегруємо Sync у ваш додаток під ключ.
Як працює ObjectBox Sync?
ObjectBox Sync використовує дельта-синхронізацію: передаються лише змінені об'єкти, а не вся база. Резолюція конфліктів налаштовується: можна обрати "останній пише", "локальний пріоритет" або кастомну логіку. Для початку достатньо встановити сервер Sync і вказати URL у клієнті.
Порівняння ObjectBox і Room
| Критерій |
ObjectBox |
Room/SQLite |
| Швидкість запису (1000 об'єктів) |
~0.5 сек |
~5-15 сек |
| Швидкість читання за ID |
~0.1 мс |
~0.2 мс |
| Реактивні запити |
DataObserver |
Flow/LiveData через DAO |
| Синхронізація |
ObjectBox Sync (commercial) |
Firebase, ручна |
| Типовий термін впровадження |
3–5 днів |
5–10 днів |
Типові проблеми та рішення
| Проблема |
Рішення |
| ID не UUID |
ObjectBox призначає Long ID автоматично — вони унікальні лише локально. Для серверної синхронізації додайте окреме строкове поле з @Index. |
| Codegen при кожній збірці |
Плагін генерує MyObjectBox.java та класи _. Якщо схема змінилася, а gradle cache не скинувся — компілятор скаржиться на невідповідність. ./gradlew clean вирішує. |
| Відкритий BoxStore — один на додаток |
Спроба відкрити другий BoxStore на той самий файл викликає виняток. Singleton через Application клас обов'язковий. |
Чому ObjectBox підходить для IoT-додатків?
IoT-пристрої генерують потік часових даних: координати, показники сенсорів, логи. ObjectBox обробляє масові вставки без затримок, а реактивні підписки дозволяють миттєво оновлювати UI. У трекері, який ми робили, запис 500 точок на хвилину не викликав лагів — на відміну від Room, де збирач сміття призводив до фризів. ObjectBox також доступний для Flutter і React Native, що спрощує кросплатформну розробку.
Що входить у роботу
- Проектування об'єктної моделі з відношеннями
- Налаштування BoxStore, codegen, оптимізація індексів
- Реалізація реактивних запитів (LiveData/Flow)
- Інтеграція ObjectBox Sync (при необхідності)
- Тестування продуктивності (бенчмарки до/після)
- Документація зі схеми та доступів
- Підтримка 30 днів після здачі
Замовте налаштування ObjectBox для вашого мобільного додатку — ми гарантуємо результат. Більше 5 років ми займаємося мобільною розробкою і реалізували 50+ проектів на ObjectBox та альтернативах. Отримайте безкоштовну оцінку — напишіть нам!
Як вибрати рішення для локального зберігання даних (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 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.