Розробка чата в мобільному додатку (один на один)
Приватний чат між двома користувачами — не «просто список повідомлень». Проблема звичайно не у відображенні, а в синхронізації станів: користувач на iOS відправив повідомлення, Android-собеседник його бачить через 3 секунди з дублем, тому що WebSocket відвалився та клієнт переотправив через REST. Або історія не завантажується при поганому з'єднанні, тому що пагінація реалізована через OFFSET та timeout бує по 500-й сторінці.
Транспорт: WebSocket або SSE
Для чата один на один достатньо WebSocket-з'єднання. На iOS — URLSessionWebSocketTask (нативно, без залежностей) або Starscream якщо потрібна гнучка обробка heartbeat. На Android — OkHttp WebSocket з коробки через Retrofit-екосистему або Ktor WebSocket Client для Kotlin Multiplatform.
Критичний момент — реконнект. WebSocket рветься при зміні мережі (Wi-Fi → 4G), при блокуванні фону iOS, при агресивному Doze Mode на Android. Потрібен exponential backoff: перший реконнект через 1s, потім 2s, 4s, 8s, cap 30s. Після реконнекта — запит missed messages з last_message_id щоб не пропустити те, що прийшло поки з'єднання було розірвано.
Firebase Realtime Database або Firestore — альтернатива власному WS-серверу для дрібних проектів. Швидко стартувати, але при складній бізнес-логіці (модерація, шифрування на рівні сервера) виникає обмеження Cloud Functions.
Як ми будуємо чат
Модель даних
Повідомлення: id, conversation_id, sender_id, body, type (text/image/file/system), status (sent/delivered/read), client_message_id (UUID генерується на клієнті), created_at. client_message_id — ключ ідемпотентності: якщо клієнт переотправляє після таймаута, сервер не створює дубль.
Conversation: id, participant_ids[], last_message_id, last_message_at. Індекс: (participant_ids, last_message_at DESC) — щоб список діалогів у користувача вибирався швидко.
Пагінація історії
Cursor-based по (created_at, id): завантажуємо останні N повідомлень, при скролі вверх передаємо before_cursor. Це працює стабільно при швидкій вставці нових повідомлень — OFFSET «пливе», коли між сторінками з'являються нові записи.
На iOS список повідомлень — UICollectionView з інвертованим layout (нові знизу): transform = CGAffineTransform(scaleX: 1, y: -1) на collectionView та кожній ячейці. При додаванні нового повідомлення insertItems з scrollToItem — без reloadData який вызивает мерцання. На Android — LazyColumn(reverseLayout: true) в Jetpack Compose.
Статусы доставки та прочитання
Delivered: сервер підтверджує отримання повідомлення (ack у WS-протоколі) та оновлює статус. Read: клієнт-отримувач відправляє read receipt коли conversation відкритий та повідомлення видимо на екрані (через Intersection Observer на вебі або через UICollectionView.indexPathsForVisibleItems на iOS).
Статуси у UI: одна галочка (sent), дві сірі (delivered), дві сині (read) — класика. Змінюємо через локальний update у DiffableDataSource без сітьового запиту.
Шифрування
Для базового end-to-end: Signal Protocol через libsignal-client (Rust-бібліотека з біндингами для iOS/Android). Ключи зберігаються в Keychain (iOS) / Android Keystore. Сервер бачить лише зашифрований blob — навіть при компрометації БД переписка не читається.
Якщо E2E не обов'язково — шифрування на транспорті (TLS 1.3) + шифрування в покої на стороні сервера достатньо для більшості випадків.
Push-сповіщення коли чат закритий
FCM (Android) та APNs (iOS). На iOS потрібен UNUserNotificationCenter + UNNotificationServiceExtension якщо потрібно показати превью зашифрованого повідомлення: Extension розшифровує payload перед показом без передачі ключів серверу.
Deep link при тапі на пуш — відкриває конкретну conversation: myapp://chat/conversation/{id}. Реалізується через UIApplicationDelegate.application(_:open:options:) або onOpenURL в SwiftUI.
Етапи та терміни
Базовий чат (WS-з'єднання, історія з пагінацією, статусы, пуші) — 5 робочих днів на одну платформу. Додавання медіа-вкладень, E2E-шифрування, індикатор набору тексту (typing indicator через WS-event) — ще 3-5 днів. Flutter — трохи швидше за рахунок єдиної кодової бази. Вартість — після аналізу вимог та цільових платформ.







