В какой‑то момент мне прилетел баг‑репорт, который идеально описывает боль 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. Сделать так, чтобы это работало при плохой сети и в офлайне.

Что в итоге есть в продукте

  • личные и групповые привычки;

  • инвайты в привычки и челленджи;

  • прогресс по участникам в группах;

  • фото‑пруфы выполнения;

  • статистика (день/неделя/год), рейтинг друзей;

  • виджет домашнего экрана списка привычек «На сегодня»;

  • офлайн‑режим с последующей синхронизацией.

Splash-экран приложения Habit Tracker
Splash‑экран приложения Habit Tracker
Главный экран «Мои привычки» (тёмная тема)
Главный экран «Мои привычки» (тёмная тема)
Экран деталей групповой привычки с прогрессом участников
Экран деталей групповой привычки с прогрессом участников
Экран статистики: уровни, активность и рейтинг друзей (тёмная тема)
Экран статистики: уровни, активность и рейтинг друзей (тёмная тема)

Стек и архитектура (без магии)Стек:

  • 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 живёт отдельно;

  • «возобновить» и «выполнить» приходят как два события;

  • клиенты могут зафиксировать промежуточное состояние.

Что пришлось сделать:

  1. При выполнении привычки всегда принудительно снимать isRestDay.

  2. Нормализовать переходы состояний в domain/data.

  3. Не позволять старому 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)