Предисловие

Я — соло-разработчик 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 при изменении mutableStateListOf

  • Room гарантирует потокобезопасность через 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. Вот почему:

  1. Независимость экранов — каждый экран полностью самодостаточен

  2. Системные переходы — Android'овские анимации Activity transition бесплатно

  3. Deep linking — каждая Activity имеет свой Intent

  4. Отладка — 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
}

Что я бы сделал иначе

  1. ViewModel — хотя бы для экранов с тяжёлой логикой (BudgetActivity)

  2. Нормальные миграции Room — destructive migration в проде — плохо

  3. Модульность — вынести feature-модули (:budget, :todo, :notes)

  4. Тесты — да, их нет. Для соло-проекта я полагаюсь на ручное тестирование

Но приложение работает, пользователи довольны, а я доволен скоростью разработки.


Цифры

Метрика

Значение

Размер APK (release, R8)

8.5 МБ

Размер AAB

14 МБ

Количество Activity

7

Количество Room-сущностей

6

Время cold start

~400ms (Pixel 6)

Минимальный SDK

26 (Android 8.0)


Ссылки


Если есть вопросы по архитектуре или хотите подискутировать про multi-Activity vs single-Activity — welcome в комментарии.

Комментарии (15)


  1. impalex
    25.02.2026 10:12

    Эмиль, остановитесь, пожалуйста. Это уже третья статья про одно и то же, без какой-либо полезной информации.

    И чем дальше, тем глубже Вы себя закапываете. Статьи можно уже разбирать для сборника "как делать не надо". Глядя на приведенный onCreate, у меня волосы дыбом встают. Смешивание UI и data. Контекст активности в Room вместо контекста приложения. Пересоздание базы при повороте экрана... можно продолжать, тут много "что не так".

    Не использование проверенных временем паттернов - это не повод для гордости.


    1. JDJ
      25.02.2026 10:12

      статьи тоже ИИ пишет похоже, прям не отличить от моего чата, понятно что человек пробует раскрутить приложение, не понятно почему он его доделать сначала не хочет.


      1. Emi_Dev Автор
        25.02.2026 10:12

        Статьи пишу сам как и код, буду открыт, запустил ранний релиз, так как у меня есть большой кредит и уже просрочка 2 месяц , да вы права делаю эти статьи чтобы продвинуть приложение, у меня нет никого близкого кто бы мог помочь финансово огромной суммой поэтому я выпустил это приложение на скорую руку максимально постарался сделать рекламу не навязчивой


      1. Emi_Dev Автор
        25.02.2026 10:12

        Продвигать иначе не получается, плей маркет требует обязательного закрытого тестирование, поэтому продвигаю как могу, спасибо за понимание:) буду рад если предложите какие нибудь плюшки и фичи что можно добавить в приложение чтобы оно было полезным


        1. JDJ
          25.02.2026 10:12

          не понял связи между плеймаркетом и продвижением, вы думаете на плеймаркете ваше приложение начнуть массово скачивать потому что там аудитория больше? если так в чём проблема пройти этот закрытый тест, я тоже начинающий разработчик, у меня 4 приложения в плеймаркете, тест проходится очень легко. за пол года на все 4 приложения у меня 200 загрузок :) там так же надо продвигать как и на русторе. а может ещё агрессивней. в магазине хуавея и то больше загрузок чем на плеймаркете, без продвижений. у меня есть опыт работы с РСЯ 6 лет , но на сайтах, я добавлял рекламу в приложения, за 2 месяца заработалось 300р и прилетел бан от яндекса без объяснений. Субъективное мнение, это пустая затея, срубить бабло по быстрому на рекламе в мобильном приложении. Мне кажется этого бабла там просто нет. Хотя я начининающий разработчик который не раскручивает свои приложения, скорее всего сильно могу ошибаться.


          1. Emi_Dev Автор
            25.02.2026 10:12

            А что посоветуете сделать?


          1. Emi_Dev Автор
            25.02.2026 10:12

            У нас в Узбекистане только плеймаркету лояльность населения, и мне будет легче продвинуть его, население нашей страны не доверяют никакому источнику за исключением плеймаркет, так как в последнее время активизировались мошенники которые скидывают АПК под предлогом разным будь то повестка в суд или же "Ты ли это на фото" после скачивания и открытия , телефон превращается в кирпич визуально но в это время с него выкачивают все данные и деньги


          1. Mavd7007
            25.02.2026 10:12

            А у вас какие-то очень локальные приложения? Просто странно, что так мало загрузок


            1. Emi_Dev Автор
              25.02.2026 10:12

              Нет, только недавно запустил


            1. JDJ
              25.02.2026 10:12

              я бы не сказал что они локальные, первое моё приложение переведено. правда с помощью gpt, на 29 языков, оно мониторит уровень мобильного сигнала и тип мобильного интернета в телефоне, а теперь и в некоторых роутерах, у него 104 загрузки на плеймаркете за пол года, и 800 загрузок на хуавее за год, 700 из них турция, незнаю почему турция. второе офлайн распознавалка речи в помощью vosk, тут тоже около 3х десятков языков, по сути под заказ делал его, не моя идея, третье видеопроигрыватель в 4 окна, камер видеонаблюдения, iptv и twitch, казалость бы прикольная на мой взгляд идея, но его вообще никто не качает, 15 загрузок за год в хуавее, 2 в плеймаркете, четвёртое хоть и нишевое, читает смски в роутерах микротик, чего не умеет родное приложение, но даже оно 20 загрузок за пол года, при 400 просмотрах на офф форуме микротика. Стараюсь делать приложения худо бедно оригинальные, по идее, а не 1000000009й будильник или ежедневник, без негатива к топик стартеру.


              1. Mavd7007
                25.02.2026 10:12

                А обновления вы часто выпускаете к ним?

                Просто я 15 декабря выпустил свое первое приложение и сейчас у меня что-то около 130 загрузок. И загрузки идут, только если примерно раз в неделю выпускать какое-то обновление. Я в январе экспериментировал 14 или даже больше дней не выпускал обновления и постепенно установки сошли на нет, а после возобновления обновлений опять пошли. (Получается, если бы не экспериментировал, то было бы ещё чуть больше установок)


                1. JDJ
                  25.02.2026 10:12

                  что вы имеете ввиду под загрузкой, нового пользователя или старого установивщего обновление ? обновление выпуская раз в пару месяцев, если нахожу баг или добавляю какую то новую функцию.


                  1. Mavd7007
                    25.02.2026 10:12

                    Нового пользователя (установку)


  1. Katou
    25.02.2026 10:12

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


    1. impalex
      25.02.2026 10:12

      Не ошибаетесь. Не знаю, у кого заказывалось, но исполнитель выполнил свою работу максимально топорно. Однотипные, одной пачкой, даже без попытки имитации органического трафика, написаны языком, которым реальные пользователи не пишут, не соответствуют реальности (постоянные утверждения про "без рекламы"), местами видны следы нейронки (когда та сбивается, и начинает путаться в символах) - " интерфيل ", " Удобно، ". Печально... Поначалу автор оставлял положительное впечатление, несмотря на все косяки (ну, учится человек, разбирается, делится опытом).