Реализация Presence (онлайн-статус, активность) для collaboration в мобильном приложении
Presence — это эфемерный слой данных о том, что пользователь делает прямо сейчас: онлайн ли он, на каком экране, что редактирует, где его курсор. В отличие от обычных данных приложения, presence не нужно хранить в БД — оно живёт только пока активно соединение. Но реализовать его правильно сложнее, чем кажется.
Почему нельзя просто хранить is_online в Firestore
Классическая ошибка: добавить поле lastSeen в документ пользователя и обновлять его каждые 30 секунд. Проблемы:
- Убийство батареи: постоянные write-операции в фоне. Android 8+ ограничивает фоновые задачи, iOS убивает Background Fetch через несколько минут.
-
Некорректный статус при крэше: приложение упало —
is_online: trueостанется до следующего обновления. - Гонки при нескольких устройствах: пользователь онлайн на телефоне и планшете. Вышел с планшета — обнулил статус, хотя телефон ещё активен.
Firebase Realtime Database: правильная реализация через onDisconnect
Firebase RTDB имеет встроенный механизм — onDisconnect(). Сервер выполняет заданную операцию автоматически при разрыве соединения, даже если клиент просто потерял сеть:
import database from '@react-native-firebase/database';
const userStatusRef = database().ref(`/status/${userId}`);
const isOfflineData = { state: 'offline', lastChanged: database.ServerValue.TIMESTAMP };
const isOnlineData = { state: 'online', lastChanged: database.ServerValue.TIMESTAMP };
// Регистрируем действие на отключение ДО установки online
await userStatusRef.onDisconnect().set(isOfflineData);
await userStatusRef.set(isOnlineData);
database.ServerValue.TIMESTAMP — серверная метка времени, не зависит от часового пояса устройства. Важно: onDisconnect регистрируется до set(isOnlineData) — иначе есть race condition, при котором клиент может отключиться между двумя вызовами.
Для поддержки нескольких устройств — счётчик активных сессий вместо булева флага:
// Используем транзакцию для атомарного increment
const sessionsRef = database().ref(`/sessions/${userId}`);
await sessionsRef.transaction(current => (current || 0) + 1);
await sessionsRef.onDisconnect().transaction(current => Math.max((current || 1) - 1, 0));
is_online = sessions > 0. Крэш на одном устройстве уменьшит счётчик через onDisconnect, не затронув другие сессии.
AppState: синхронизация с жизненным циклом iOS/Android
import { AppState, AppStateStatus } from 'react-native';
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
if (nextState === 'active') {
userStatusRef.onDisconnect().set(isOfflineData);
userStatusRef.set(isOnlineData);
} else if (nextState === 'background' || nextState === 'inactive') {
userStatusRef.set(isOfflineData);
userStatusRef.onDisconnect().cancel(); // отменяем, уже сделали вручную
}
});
return () => subscription.remove();
}, []);
На Android при background у вас есть несколько секунд до того, как система заморозит JS-тред. userStatusRef.set() — асинхронная операция, не гарантирована. onDisconnect() как fallback обязателен.
Типизированный presence с дополнительным контекстом
Помимо online/offline, часто нужно знать, что именно делает пользователь:
type PresenceState = {
status: 'online' | 'idle' | 'offline';
currentScreen: string | null;
editingItemId: string | null;
lastChanged: number;
};
idle — пользователь открыл приложение, но 5+ минут не касался экрана. Детектируется через PanResponder или TouchableWithoutFeedback на корневом компоненте с debounce-таймером.
Отображение: аватары с индикатором
В список участников с presence-данными добавляем цветной бейдж:
- Зелёный:
status === 'online' - Жёлтый:
status === 'idle' - Серый:
status === 'offline', показываемlastChangedкак «был онлайн N минут назад»
Нюанс: не обновляйте lastChanged при каждом изменении presence — только при смене status. Иначе список будет перерендериваться каждые несколько секунд для каждого активного пользователя.
Оценка
Firebase RTDB presence с поддержкой нескольких устройств, idle-detection и UI-компонентом: 1–3 недели. Custom WebSocket presence на собственном бэкенде: 2–4 недели.







