В какой‑то момент мне прилетел баг‑репорт, который идеально описывает боль social/mobile приложений:
«Я поставила в групповой привычке „выходной“, потом нажала „возобновить“ и выполнила. У меня всё ОК. У подруги — у меня до сих пор „выходной“».
То есть локально — одно состояние, у других участников — другое. Не UI‑баг, не «кнопка не нажимается». Чистая проблема консистентности данных.
Ниже — разбор, как я за ~6 месяцев сделал Android‑приложение Habit Tracker со всеми основными функциями, геймификацией, виджетом домашнего экрана, а так же social‑функциями. Расскажу почему основная сложность была в синхронизации Room↔Firestore, и где AI реально помогает, а где без инженерной дисциплины всё развалится.
Приложение в RuStore: [Habit Tracker](https://www.rustore.ru/catalog/app/com.habittracker)
TL;DR
Идея: собрать «лучшее из разных трекеров» + добавить социальные функции, которых обычно не хватает.
Архитектура:
Clean + MVVM,Room-first,Outbox,Realtime merge.Основная сложность: не экраны, а корректные multi‑user/offline состояния. ‑ AI использовал как ускоритель, а не как «автопилот разработки».
Откуда взялась идея
Идею разработать такое приложение мне подкинул друг. До этого я даже и не слышал вообще о существовании трекеров привычек, не знал что такое вообще есть и как, для каких целей и для чего такие приложения используют. А мой друг как раз искал для себя такое и обратился ко мне со своей идеей. Он прошёлся по нескольким трекерам привычек и в каждом нашёл «что‑то полезное», но полного набора того, чего ему бы хотелось видеть для себя, в таких трекерах не было нигде. И тогда, объяснив свои потребности, он предложил мне создать самом такое приложение. Концепт состоял в том, что бы в приложении было всё, что ему нужно: личные привычки, с возможностью отмечать их выполнение или указывать сколько раз было сделано то или иное действие привычки (отжался 20 раз, или выпил 8 стаканов воды в день и тому подобное), групповые привычки — привычки, которые можно выполнять совместно с друзьями, следить за прогрессом друг друга, напоминать друг другу («пнуть друга») о выполнении привычки, устраивать челленджи между друзьями по тем или иным привычкам, добавлять фото‑подтверждения, комментарии, и для интереса — геймификация (очки/уровни/достижения).
Ок, вызов принят!
Я взял это как продуктовую гипотезу:
1. Собрать базу: личные привычки, статистика, напоминания, прогресс.
2. Добавить social‑слой:
друзья и групповые привычки;
челленджи;
«пнуть друга»;
фото‑подтверждения + комментарии;
геймификацию (очки/уровни/достижения).
3. Сделать так, чтобы это работало при плохой сети и в офлайне.
Что в итоге есть в продукте
личные и групповые привычки;
инвайты в привычки и челленджи;
прогресс по участникам в группах;
фото‑пруфы выполнения;
статистика (день/неделя/год), рейтинг друзей;
виджет домашнего экрана списка привычек «На сегодня»;
офлайн‑режим с последующей синхронизацией.




Стек и архитектура (без магии)Стек:
Kotlin, Coroutines, Flow
Jetpack Compose, Navigation Compose
Hilt
Room (локально)
Firebase Auth, Firestore, FCM
WorkManager
Crashlytics, Firebase Performance, Yandex AppMetrica
Слои:presentation (Compose, ViewModel)domain (use-cases, модели, интерфейсы)data (Room/Firestore, sync, repository)
Почему так:
предсказуемые границы ответственности;
проще тестировать переходы состояний;
проще локализовывать race conditions в sync-слое.---
Главный технический узел: синхронизация Room ↔ Firestore
Я прошёл несколько итераций и остановился на схеме Room-first + Outbox + Realtime merge.
1) Сначала Room, потом outbox
Любое действие пользователя сразу отражается локально, а затем уходит в очередь синхронизации.
suspend fun enqueueUpsert(
entityType: String,
entityId: String,
payloadJson: String,
updatedAt: Long,
operationId: String = UUID.randomUUID().toString()
) = withContext(Dispatchers.IO) {
outboxDao.insert(
SyncOutboxEntity(
operationId = operationId,
entityType = entityType,
entityId = entityId,
opType = "UPSERT",
payloadJson = payloadJson,
updatedAt = updatedAt
)
)
}
2) WorkManager + ручной KickSync
Периодический воркер разгружает outbox, но после пользовательских действий я дополнительно «пинаю» синк, чтобы изменения не висели только локально.
override suspend fun doWork(): Result { return try { syncManager.drainPending() Result.success() } catch (e: Exception) { Result.retry() }}
3) Pending-guard на входящем realtime
Если безусловно мержить Firestore в Room, можно затирать локальные более свежие изменения.
val hasPending = syncManager.hasPendingOperations("habit_entry", entityId)if (hasPending) { continue // не перетираем локальные pending-изменения}
Плюс LWW-логика по времени обновления.
Связка pending guard + LWW убрала заметную часть рассинхронов.---
Разбор реального бага: «выходной → возобновить → выполнить»
Почему этот кейс часто ломается:
флаг isRestDay живёт отдельно;
«возобновить» и «выполнить» приходят как два события;
клиенты могут зафиксировать промежуточное состояние.
Что пришлось сделать:
При выполнении привычки всегда принудительно снимать isRestDay.
Нормализовать переходы состояний в domain/data.
Не позволять старому remote-снапшоту затирать локальное pending-состояние.Ключевой фрагмент:
val updatedEntry = existingEntry.copy( completed = true, completedAt = now, note = note, isRestDay = false, updatedAt = today)updateEntry(updatedEntry)
После фикса кейс перестал расходиться между участниками (проверял руками на двух клиентах + прогонял тесты).
Soft-delete: почему это не лишняя сложность
Для части сущностей я использую tombstones (isActive=false, deletedAtMillis) вместо немедленного hard delete.
Это закрывает неприятные сценарии:
одно устройство уже удалило;
второе долго было офлайн;
после reconnection данные не должны «воскресать» или теряться.Для social-приложения это стоит дополнительной сложности.
Виджет «Сегодня»: маленькая фича, много нюансов
На виджете важны две вещи:
очевидный ручной refresh;
предсказуемый автоrefresh.
Я поменял расписание автообновлений:
00:10
15:00
отдельный force refresh в 05:00
schedule(context, ACTION_REFRESH_MIDNIGHT, requestCode = 2001, hour = 0, minute = 10)
schedule(context, ACTION_REFRESH_AFTERNOON, requestCode = 2002, hour = 15, minute = 0)
schedule(context, ACTION_FORCE_REFRESH_MORNING, requestCode = 2003, hour = 5, minute = 0)
Плюс добавил явную иконку refresh рядом с заголовком «Сегодня»: тап по заголовку пользователи не считывали как действие.
Наблюдаемость: иначе вы разрабатываете вслепую
Использую:
Crashlytics для падений и stacktrace;
AppMetrica для продуктовых событий и ошибок;
Firebase Performance для перфоманса.
Ошибки отправляются в оба канала:
crashlytics.recordException(throwable)AppMetrica.reportEvent(AnalyticsEvents.ERROR_OCCURRED, mapOf(...))AppMetrica.reportError(message ?: "Error", throwable)
Текущий срез (RuStore ~1.5 месяца):
около 100 пользователей;
11–14 активных в среднем.
Где в этом AI, и почему это не «нагенерил и выкатил»
AI я использовал активно, но как ускоритель:
быстрые прототипы и дизайн UI/UX;
для рефакторинга и обновления зависимостей «без конфликтов»;
генерация тест‑кейсов и вариантов решения;
ускорение рутинных правок.
Где AI не заменяет разработчика:
проектирование state‑machine (частично);
merge/conflict‑правила;
проверка race conditions;
финальные архитектурные решения.
Коротко: AI отлично ускоряет реализацию/рефакторинг, после грамотного проектирования архитектуры, а так же помогает быстро поправить ошибки при сборке и выполнить рутинные задачи и тесты, но не принимает за вас ответственные инженерные решения.
Про то какие AI инструменты и модели я использовал, и для каких кейсов, в рамках данной статьи я рассматривать, пожалуй, не буду, так как этот материал заслуживает отдельной статьи. Мне есть что рассказать и чем поделиться на основе полученного опыта во время разработки данного проекта, но это будет уже другая история.
Если вам будет интересно об этом послушать, напишите об этом в комментариях и я постараюсь подготовить для вас следующую статью уже на тему AI инструментов, LLM моделей, и в каких кейсах какие из них лучше всего себя показывали на основе примеров разработки приложения Habit Tracker.
Что оказалось самым дорогим по времени
Не экраны и не анимации.
Больше всего времени забрали:
1. консистентность в multi‑user сценариях;
2. офлайн + последующая синхронизация без потери пользовательских действий;
3. UX для опасных операций (сбросы/массовые действия/подтверждения).
Именно здесь проект перестаёт быть «просто pet‑проектом».
Что планирую дальше
держать стабильность релизов;
отслеживать метрики крашей и ошибок, оперативно исправляя их;
развивать social‑механики и челленджи;
расширять аналитику поведения;
дорабатывать виджетные сценарии (по необходимости);
аккуратно внедрять монетизацию: рекламу/платные фичи.
Вывод
Сделать трекер привычек — несложно. Сложно сделать его социальным, офлайн‑устойчивым и консистентным.
И второй вывод:
AI — это не «вместо инженера», а «вместе с инженером». Если критическое мышление не выключено, можно сильно ускориться без потери качества.
Если вам будет интересно, в следующей статье я могу сделать более узкий deep dive по sync‑слою: схема потоков, конфликтные кейсы и тесты на них. Пишите в комментариях — о чём бы вы хотели, что бы я по подробнее описал по технической части из функционала и фичей этого проекта.
Материалы
‑ Приложение в RuStore: [HabitTracker](https://www.rustore.ru/catalog/app/com.habittracker)
