Предисловие
Я — соло-разработчик Android-приложения Todo Budget. Это не очередной TODO-лист и не очередной трекер расходов. Это комбайн, в котором живут задачи, заметки, бюджет, аналитика, помодоро-таймер и цели накоплений — всё в одном APK весом 8.5 МБ. В предыдущих статьях я уже упоминал свое приложение но уже в готовом виде, теперь я бы хотел рассказать как я начинал.
В этой статье я расскажу, как устроена архитектура приложения и почему я принял те решения, которые принял. Будет код, будут грабли, будет честный разбор.
Стек
Kotlin 1.9.22 Jetpack Compose + Material 3 Room (SQLite) DataStore (Preferences) Foreground Service AppWidgetProvider Yandex Mobile Ads SDK Gradle 8.2, AGP 8.2.2, compileSdk 35
Никаких Dagger/Hilt, никакого Retrofit, никакого Firebase. Всё локально. Данные пользователя не покидают устройство.
Архитектура: почему не MVVM и не Clean Architecture
Когда у тебя одноэкранные Activity с Compose-контентом внутри, а данные лежат в Room — городить слой UseCase'ов и Repository для каждой сущности — это оверинжиниринг. Я выбрал подход Activity + Room DAO + State, где:
Каждый экран — отдельная
ComponentActivityСостояние хранится в
mutableStateOf/mutableStateListOfпрямо вsetContent {}Room DAO вызывается через
LaunchedEffectиcoroutineScope
class BudgetActivity : ComponentActivity() { private lateinit var db: AppDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) db = Room.databaseBuilder(this, AppDatabase::class.java, "todobudgettodo_database") .fallbackToDestructiveMigration() .build() setContent { val transactions = remember { mutableStateListOf<Transaction>() } val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { transactions.addAll(db.transactionDao().getAll()) } // UI... } } }
Почему это работает:
Одиночная разработка → минимум абстракций → быстрее итерации
Compose сам реактивно перерисовывает UI при изменении
mutableStateListOfRoom гарантирует потокобезопасность через
suspendфункции
Почему это может не работать в масштабе:
Тестируемость — сложнее мокать DAO напрямую
При увеличении команды → нужны чёткие контракты между слоями
Я осознанно принял этот trade-off. Для соло-проекта с 6 экранами это оправдано.
Room: одна база — шесть сущностей
@Database( entities = [ Task::class, Note::class, Transaction::class, Category::class, SavingsGoal::class, RecurringTransaction::class ], version = 9 ) abstract class AppDatabase : RoomDatabase() { abstract fun taskDao(): TaskDao abstract fun noteDao(): NoteDao abstract fun transactionDao(): TransactionDao abstract fun categoryDao(): CategoryDao abstract fun savingsGoalDao(): SavingsGoalDao abstract fun recurringTransactionDao(): RecurringTransactionDao }
Миграции: слон в комнате
Честно: я использую fallbackToDestructiveMigration(). Для продакшена с сотнями тысяч пользователей это неприемлемо. Для приложения версии 4.0 с растущей базой пользователей — это осознанный компромисс, который я планирую убрать в 5.0, написав нормальные Migration(8, 9).
Почему пока так:
Схема менялась радикально между версиями (добавлялись целые таблицы)
Потеря данных для пользователей v3 → v4 минимальна (приложение предупреждает)
Писать миграции для 9 версий при одном разработчике — дорого по времени
Навигация: Activity vs Single Activity
Controversial take: я использую несколько Activity вместо одной с NavHost. Вот почему:
Независимость экранов — каждый экран полностью самодостаточен
Системные переходы — Android'овские анимации Activity transition бесплатно
Deep linking — каждая Activity имеет свой Intent
Отладка — stack trace сразу показывает, где ты
// Из MainActivity startActivity(Intent(this, BudgetActivity::class.java)) startActivity(Intent(this, TodoActivity::class.java)) startActivity(Intent(this, NotesActivity::class.java)) startActivity(Intent(this, ProductivityToolsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
Compose Navigation — отличный инструмент. Но мне не нужны shared element transitions между TODO-списком и бюджетом. Это концептуально разные экраны.
Dark Theme через DataStore
Material 3 поддерживает динамические цвета, но я хотел один toggle в настройках:
// Preferences.kt — общий для всех Activity object Preferences { private val Context.dataStore by preferencesDataStore(name = "settings") object PrefsKeys { val DARK_THEME = booleanPreferencesKey("dark_theme") val PIN_ENABLED = booleanPreferencesKey("pin_enabled") val PIN_CODE = stringPreferencesKey("pin_code") } fun getDataStore(context: Context) = context.dataStore }
Каждый Activity читает тему:
val darkTheme = remember { mutableStateOf(false) } LaunchedEffect(Unit) { Preferences.getDataStore(context).data.collect { prefs -> darkTheme.value = prefs[Preferences.PrefsKeys.DARK_THEME] ?: false } } TodoBudgetTheme(darkTheme = darkTheme.value) { // Content }
Что я бы сделал иначе
ViewModel — хотя бы для экранов с тяжёлой логикой (BudgetActivity)
Нормальные миграции Room — destructive migration в проде — плохо
Модульность — вынести feature-модули (:budget, :todo, :notes)
Тесты — да, их нет. Для соло-проекта я полагаюсь на ручное тестирование
Но приложение работает, пользователи довольны, а я доволен скоростью разработки.
Цифры
Метрика |
Значение |
|---|---|
Размер APK (release, R8) |
8.5 МБ |
Размер AAB |
14 МБ |
Количество Activity |
7 |
Количество Room-сущностей |
6 |
Время cold start |
~400ms (Pixel 6) |
Минимальный SDK |
26 (Android 8.0) |
Ссылки
GitHub: github.com/emil-a-dev/todofin
Если есть вопросы по архитектуре или хотите подискутировать про multi-Activity vs single-Activity — welcome в комментарии.
Комментарии (15)

Katou
25.02.2026 10:12Такое чувство, что у вас в RuStore закрученные отзывы. Надеюсь, я ошибаюсь

impalex
25.02.2026 10:12Не ошибаетесь. Не знаю, у кого заказывалось, но исполнитель выполнил свою работу максимально топорно. Однотипные, одной пачкой, даже без попытки имитации органического трафика, написаны языком, которым реальные пользователи не пишут, не соответствуют реальности (постоянные утверждения про "без рекламы"), местами видны следы нейронки (когда та сбивается, и начинает путаться в символах) - " интерфيل ", " Удобно، ". Печально... Поначалу автор оставлял положительное впечатление, несмотря на все косяки (ну, учится человек, разбирается, делится опытом).
impalex
Эмиль, остановитесь, пожалуйста. Это уже третья статья про одно и то же, без какой-либо полезной информации.
И чем дальше, тем глубже Вы себя закапываете. Статьи можно уже разбирать для сборника "как делать не надо". Глядя на приведенный onCreate, у меня волосы дыбом встают. Смешивание UI и data. Контекст активности в Room вместо контекста приложения. Пересоздание базы при повороте экрана... можно продолжать, тут много "что не так".
Не использование проверенных временем паттернов - это не повод для гордости.
JDJ
статьи тоже ИИ пишет похоже, прям не отличить от моего чата, понятно что человек пробует раскрутить приложение, не понятно почему он его доделать сначала не хочет.
Emi_Dev Автор
Статьи пишу сам как и код, буду открыт, запустил ранний релиз, так как у меня есть большой кредит и уже просрочка 2 месяц , да вы права делаю эти статьи чтобы продвинуть приложение, у меня нет никого близкого кто бы мог помочь финансово огромной суммой поэтому я выпустил это приложение на скорую руку максимально постарался сделать рекламу не навязчивой
Emi_Dev Автор
Продвигать иначе не получается, плей маркет требует обязательного закрытого тестирование, поэтому продвигаю как могу, спасибо за понимание:) буду рад если предложите какие нибудь плюшки и фичи что можно добавить в приложение чтобы оно было полезным
JDJ
не понял связи между плеймаркетом и продвижением, вы думаете на плеймаркете ваше приложение начнуть массово скачивать потому что там аудитория больше? если так в чём проблема пройти этот закрытый тест, я тоже начинающий разработчик, у меня 4 приложения в плеймаркете, тест проходится очень легко. за пол года на все 4 приложения у меня 200 загрузок :) там так же надо продвигать как и на русторе. а может ещё агрессивней. в магазине хуавея и то больше загрузок чем на плеймаркете, без продвижений. у меня есть опыт работы с РСЯ 6 лет , но на сайтах, я добавлял рекламу в приложения, за 2 месяца заработалось 300р и прилетел бан от яндекса без объяснений. Субъективное мнение, это пустая затея, срубить бабло по быстрому на рекламе в мобильном приложении. Мне кажется этого бабла там просто нет. Хотя я начининающий разработчик который не раскручивает свои приложения, скорее всего сильно могу ошибаться.
Emi_Dev Автор
А что посоветуете сделать?
Emi_Dev Автор
У нас в Узбекистане только плеймаркету лояльность населения, и мне будет легче продвинуть его, население нашей страны не доверяют никакому источнику за исключением плеймаркет, так как в последнее время активизировались мошенники которые скидывают АПК под предлогом разным будь то повестка в суд или же "Ты ли это на фото" после скачивания и открытия , телефон превращается в кирпич визуально но в это время с него выкачивают все данные и деньги
Mavd7007
А у вас какие-то очень локальные приложения? Просто странно, что так мало загрузок
Emi_Dev Автор
Нет, только недавно запустил
JDJ
я бы не сказал что они локальные, первое моё приложение переведено. правда с помощью gpt, на 29 языков, оно мониторит уровень мобильного сигнала и тип мобильного интернета в телефоне, а теперь и в некоторых роутерах, у него 104 загрузки на плеймаркете за пол года, и 800 загрузок на хуавее за год, 700 из них турция, незнаю почему турция. второе офлайн распознавалка речи в помощью vosk, тут тоже около 3х десятков языков, по сути под заказ делал его, не моя идея, третье видеопроигрыватель в 4 окна, камер видеонаблюдения, iptv и twitch, казалость бы прикольная на мой взгляд идея, но его вообще никто не качает, 15 загрузок за год в хуавее, 2 в плеймаркете, четвёртое хоть и нишевое, читает смски в роутерах микротик, чего не умеет родное приложение, но даже оно 20 загрузок за пол года, при 400 просмотрах на офф форуме микротика. Стараюсь делать приложения худо бедно оригинальные, по идее, а не 1000000009й будильник или ежедневник, без негатива к топик стартеру.
Mavd7007
А обновления вы часто выпускаете к ним?
Просто я 15 декабря выпустил свое первое приложение и сейчас у меня что-то около 130 загрузок. И загрузки идут, только если примерно раз в неделю выпускать какое-то обновление. Я в январе экспериментировал 14 или даже больше дней не выпускал обновления и постепенно установки сошли на нет, а после возобновления обновлений опять пошли. (Получается, если бы не экспериментировал, то было бы ещё чуть больше установок)
JDJ
что вы имеете ввиду под загрузкой, нового пользователя или старого установивщего обновление ? обновление выпуская раз в пару месяцев, если нахожу баг или добавляю какую то новую функцию.
Mavd7007
Нового пользователя (установку)