В какой‑то момент мне прилетел баг‑репорт, который идеально описывает боль social/mobile приложений:
> «Я поставила в групповой привычке „выходной“, потом нажала „возобновить“ и выполнила. У меня всё ок. У подруги — у меня до сих пор „выходной“».
То есть локально — одно состояние, у других участников — другое. Не UI‑баг, не «кнопка не нажимается». Чистая проблема консистентности данных.
Ниже — разбор, как я за ~6 месяцев сделал Android‑приложение Habit Tracker со всеми основными функциями, геймификацией, виджетом домашнего экрана, а так же social‑функциями. Расскажу почему основная сложность была в синхронизации Room↔Firestore, и где AI реально помогает, а где без инженерной дисциплины всё развалится.
Приложение в RuStore: [Habit Tracker](https://www.rustore.ru/catalog/app/com.habittracker) <!‑more‑→
‑- ## 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
Слои:
```textpresentation (Compose, ViewModel)domain (use-cases, модели, интерфейсы)data (Room/Firestore, sync, repository)
```Почему так:
- предсказуемые границы ответственности;- проще тестировать переходы состояний;- проще локализовывать race conditions в sync-слое.---## Главный технический узел: синхронизация Room ↔ Firestore
Я прошёл несколько итераций и остановился на схеме Room-first + Outbox + Realtime merge.
### 1) Сначала Room, потом outbox
Любое действие пользователя сразу отражается локально, а затем уходит в очередь синхронизации.
```kotlinsuspend 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, но после пользовательских действий я дополнительно «пинаю» синк, чтобы изменения не висели только локально.
```kotlinoverride suspend fun doWork(): Result { return try { syncManager.drainPending() Result.success() } catch (e: Exception) { Result.retry() }}
```### 3) Pending-guard на входящем realtime
Если безусловно мержить Firestore в Room, можно затирать локальные более свежие изменения.
```kotlinval hasPending = syncManager.hasPendingOperations("habit_entry", entityId)if (hasPending) { continue // не перетираем локальные pending-изменения}
```Плюс LWW-логика по времени обновления. Связка pending guard + LWW убрала заметную часть рассинхронов.---## Разбор реального бага: «выходной → возобновить → выполнить»
Почему этот кейс часто ломается:
- флаг isRestDay живёт отдельно;- «возобновить» и «выполнить» приходят как два события;- клиенты могут зафиксировать промежуточное состояние.
Что пришлось сделать:
1. При выполнении привычки всегда принудительно снимать isRestDay.2. Нормализовать переходы состояний в domain/data.3. Не позволять старому remote-снапшоту затирать локальное pending-состояние.Ключевой фрагмент:
```kotlinval 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
```kotlinschedule(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 для перфоманса.
Ошибки отправляются в оба канала:
```kotlincrashlytics.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)
Источник

![[Перевод] Люди против нейросетей: как Сэм Альтман обесценивает человеческий интеллект](https://mexc-rainbown-activityimages.s3.ap-northeast-1.amazonaws.com/banner/F20250806143935710fjLhu90Kl0ipEV.png)
