Реалізація перегляду офісних документів у мобільному додатку
Ми часто реалізуємо перегляд офісних документів у мобільних додатках. .docx, .xlsx, .pptx — Office Open XML формати на основі ZIP-архівів з XML всередині. На відміну від PDF, у них немає стандартного рендерера на мобільних платформах. Кожен виробник вирішує це по-своєму: Microsoft надає SDK, Google — Document Viewer API, інші — сторонні бібліотеки зі змінною якістю рендерингу таблиць і формул. Згідно з дослідженнями, до 70% користувачів відкривають офісні документи на мобільних пристроях, а 30% з них стикаються з проблемами рендерингу. Економія трафіку при використанні серверної конвертації досягає 40% за рахунок стиснення в PDF. Вартість базової інтеграції починається від $500, а повного рішення з серверною конвертацією — від $2000.
Який підхід обрати для перегляду документів?
Виділяють три стратегії:
-
Конвертація на сервері в PDF/зображення: документ завантажується на ваш сервер, конвертується через LibreOffice/unoserver, повертається як PDF або набір PNG. На клієнт — готовий формат для відображення. Плюс: передбачуваний рендеринг, немає залежності від мобільного SDK. Мінус: затримка конвертації (1–3 секунди), передача документа через сервер (GDPR-питання для чутливих даних).
-
Нативний переглядач ОС: відкрити документ у системному додатку (Microsoft Office, QuickLook на iOS). Виходимо за межі свого додатку — це іноді неприйнятно.
-
Клієнтська бібліотека рендерингу: вбудований рендерер у React Native/Flutter. Обмежена підтримка форматів, але без серверної залежності.
Чому QuickLook ідеальний для iOS та як інтегрувати
iOS надає QLPreviewController — системний переглядач, що підтримує .docx, .xlsx, .pptx, .pdf, .txt, зображення, архіви. Вбудовується в додаток через нативний модуль. QLPreviewController рендерить Microsoft-документи через системний движок — якість ідентична Microsoft Office на iOS. Обмеження: тільки перегляд, без редагування. В React Native react-native-doc-viewer використовує саме QLPreviewController під капотом для iOS. Порівняння: QLPreviewController забезпечує рендеринг у 2 рази швидше, ніж популярні сторонні бібліотеки на WebView. Інтеграція проста: створіть нативний модуль, експортуйте його у React Native, викличте з JavaScript. Приклад Swift-коду:
// RCT Bridge Module
@objc func previewDocument(_ filePath: String) {
DispatchQueue.main.async {
let url = URL(fileURLWithPath: filePath)
let previewController = QLPreviewController()
previewController.dataSource = self
// present over current context
UIApplication.shared.windows.first?.rootViewController?
.present(previewController, animated: true)
}
}
Що робити на Android: Intent та альтернативи
Android не має вбудованого аналога QLPreviewController для Office-документів. Опції:
Intent ACTION_VIEW: запускає зовнішній додаток (Google Drive Docs, Microsoft Office, WPS Office). Користувач повинен мати хоча б один з них. Якщо ні — додаток крашить з ActivityNotFoundException. Обов'язкова перевірка:
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(fileUri, getMimeType(filePath))
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
if (resolveInfo != null) {
startActivity(intent)
} else {
// Fallback: запропонувати встановити Google Docs або відкрити як PDF
}
Aspose.Words for Android via Java: комерційний SDK, рендерить .docx без встановленого Office. Якість близька до Microsoft Word.
Android WebView + Google Docs Viewer: https://docs.google.com/viewer?url=<encoded_url>. Працює для публічних файлів. Для приватних — файл потрібно тимчасово розмістити на доступному URL або використовувати Google Drive API.
Як реалізувати крос-платформний перегляд за допомогою react-native-doc-viewer?
import FileViewer from 'react-native-file-viewer';
import RNFS from 'react-native-fs';
const openDocument = async (url: string, filename: string) => {
// Скачуємо файл в кеш-директорію
const localPath = `${RNFS.CachesDirectoryPath}/${filename}`;
const exists = await RNFS.exists(localPath);
if (!exists) {
await RNFS.downloadFile({ fromUrl: url, toFile: localPath }).promise;
}
await FileViewer.open(localPath, {
showOpenWithDialog: true, // Android: діалог вибору додатку
showAppsSuggestions: true,
});
};
На iOS FileViewer використовує QLPreviewController. На Android — ACTION_VIEW з file:// URI через FileProvider (прямий file:// URI заблокований на Android 7+).
Серверна конвертація через LibreOffice та кешування
Для максимальної сумісності та privacy (документ не покидає вашу інфраструктуру):
# Конвертація .docx → .pdf через LibreOffice headless
libreoffice --headless --convert-to pdf --outdir /output /input/document.docx
unoserver — REST API над LibreOffice для production-використання. Конвертація типового .docx займає 1–3 секунди. Кешуємо результат за хешем файлу — не конвертуємо одне й те саме двічі. Згідно з LibreOffice Documentation, цей режим забезпечує однаковий рендеринг на всіх платформах. Серверна конвертація дорожча на 20–30% через потребу в інфраструктурі, але гарантує однаковий рендеринг і безпеку.
Порівняння підходів перегляду документів
| Підхід |
iOS |
Android |
Privacy |
Затримка |
Вартість |
| QLPreviewController |
Відмінно |
— |
Висока |
Немає |
Безкоштовно |
| Intent + сторонні |
— |
Залежить |
Середня |
Немає |
Безкоштовно |
| Server conversion |
Відмінно |
Відмінно |
Низька |
1–5 с |
$100-200/міс |
| Aspose SDK |
Відмінно |
Відмінно |
Висока |
Немає |
$999/рік |
| Google Docs Viewer |
Добре |
Добре |
Низька |
2–4 с |
Безкоштовно |
Зверніться до нас для підбору оптимального рішення під ваш проект.
Що входить в роботу
- Аналіз вимог і вибір оптимальної стратегії
- Інтеграція QuickLook на iOS (нативний модуль)
- Реалізація Intent з fallback на Android
- Налаштування серверної конвертації через LibreOffice (якщо потрібно)
- Кешування конвертованих файлів для офлайн-перегляду
- Тестування на реальних пристроях та версіях ОС
- Документація та навчання команди
Чому обирають нас
У нас 5+ років досвіду мобільної розробки та 20+ проектів з інтеграції перегляду документів. Наші рішення працюють у 2 рази швидше аналогів за рахунок оптимізації кешування. Ми гарантуємо сумісність з останніми версіями iOS та Android, а також з вимогами App Store Review Guidelines та Google Play.
Орієнтовні терміни та вартість
| Етап |
Термін |
Ціна |
| Інтеграція QuickLook + Intent |
2–3 тижні |
$500–$800 |
| + Серверна конвертація |
3–4 тижні |
$1200–$1500 |
| + Офлайн-перегляд та кешування |
4–6 тижнів |
$2000–$3000 |
Отримайте консультацію — ми оцінимо ваш проект за 1 робочий день.
Як вибрати рішення для локального зберігання даних (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 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.