Разработка чата в мобильном приложении (один на один)
Приватный чат между двумя пользователями — не «просто список сообщений». Здесь проблема обычно не в отображении, а в синхронизации состояний: пользователь на 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 — немного быстрее за счёт единой кодовой базы. Стоимость — после анализа требований и целевых платформ.







