Ответ
Чистую архитектуру (Clean Architecture, MVVM/MVI), модульность, Kotlin-first подход.
Обязательно:
- DI (Hilt/Dagger)
- Coroutines/Flow
- Jetpack Compose (если новый проект)
- Unit/UI тесты (MockK, Espresso)
- CI/CD (GitHub Actions/Bitrise)
Пример структуры модулей:
:app // entry point
:core // utils, extensions
:data // repo, API (Retrofit), DB (Room)
:domain // use cases, models
:features // feature modules
Бонус:
- Dynamic Delivery
- Multi-module navigation
- Kotlin DSL для Gradle
Избегать: God-activity, хардкода, legacy-кода без миграции.
Ответ 18+ 🔞
А, ну вот, опять про чистую архитектуру спрашивают. Давайте я вам, как человек, который уже на этом собаку съел (и не одну), на пальцах объясню, чтобы не было как в том анекдоте про «у меня тут всё по SOLID, только I — лишняя».
Смотрите сюда. Вы говорите «чистая архитектура» — я уже чувствую, как у половины читателей глаза стекленеют. «О, опять эти слои, use cases, блядь, овердохуища бойлерплейта». Спокойно. Не надо бздеть. Идея-то проще, чем кажется: твой код должен быть таким, чтобы через полгода, когда продукт-менеджер придет с криком «срочно меняем API провайдера погоды на тот, что с горбатым гномом в логотипе», ты не орал «ёпта, да я тут полгода переписывать буду!», а просто пошел в один модуль :data, нашел там WeatherRemoteDataSource, выкинул один ретрофит-сервис и впендюрил другой. И всё. Без танцев с бубном вокруг всей разъебанной кодобазы.
Вот смотрите на список обязательного. Это не просто модные слова, это ваш щит и меч.
DI (Hilt/Dagger). Без этого — вообще никуда. Это как фундамент. Представьте, вы в каждом экране создаете репозиторий, а в репозитории — датасорс, а в датасорсе — ретрофит с кучей зависимостей. Это пиздец, чувак. Получится спагетти-код, который через месяц сам черт не разберет. Hilt сейчас — это вообще сказка. Аннотацию @HiltAndroidApp в аппликейшен, @AndroidEntryPoint во вьюмодели или активити — и он тебе всё проинжектит. Красота. Главное — не превращай его в магический черный ящик, а понимай, что и откуда берется. А то будет «оно не работает, ёб твою мать!», а ты просто скоуп не тот выбрал.
Coroutines/Flow. AsyncTask и RxJava с их сложностями — это уже в прошлом, как Nokia 3310. Хотя и крутая была зверюга. Корутины — это наше всё. viewModelScope.launch, flow, collectAsStateWithLifecycle в Compose. Это позволяет писать асинхронный код, который выглядит как последовательный. И не надо этих ебаных колбэков на колбэках, где в итоге onSuccess внутри onResponse, который внутри onComplete. Ужас просто. Flow — это стримы данных. Вьюмодель говорит: «у меня тут StateFlow<UiState>», а UI просто на него подписывается и рисует. Идеально.
Jetpack Compose. Если проект новый — даже не думайте. XML с этими бесконечными констрейнтами — это как ехать на Запорожце, когда вокруг уже Теслы. Compose — это декларативный подход. Ты описываешь, как интерфейс должен выглядеть при определенных данных, а система сама решает, что и когда перерисовать. Плюс писать в десять раз быстрее. Да, есть свои грабли (например, с рекомпозицией), но игра стоит свеч. mutableStateOf — твой лучший друг.
Тесты (MockK, Espresso). А вот тут многие начинают делать вид, что пишут, а на деле — // TODO: add tests later. Потом этот later никогда не наступает. И когда что-то ломается, ты сидишь и гадаешь: это в новой фиче косяк или в старой, которую ты зацепил? Unit-тесты на use cases и вьюмодели — это must have. MockK — отличная библиотека для моков в котлине. UI-тесты (хоть они и медленные) — штука хорошая для критичных сценариев. Хоть что-то должно быть покрыто, иначе доверия ебать ноль к своему же коду.
CI/CD (GitHub Actions/Bitrise). Чтобы не было «а у меня на машине работало!». Настроил пайплайн: пулл-реквест создался -> запустились линтеры, юнит-тесты -> если всё ок, можно мержить -> после мержа в main собирается билд для тестировщиков. Автоматизация — она от греха подальше. Руками это делать — смертный грех для разработчика.
Про структуру модулей
Вот эта схема — это не просто так, это и есть суть.
:app // Точка входа. Только манифест, тема, и запуск навигации. Больше НИ-ЧЕ-ГО.
:core // Всякая общая хрень: утилиты, extensions, сериализаторы, диспетчеры потоков.
:data // Вот тут живут данные. Retrofit, Room, DataSource-ы, RepositoryImplementation.
:domain // Самое сердце. Use cases (интеракторы), модели бизнес-логики, интерфейсы репозиториев.
:features // Каждая фича — отдельный модуль. `:feature-auth`, `:feature-profile`, `:feature-feed`.
Почему так? Принцип зависимостей. Модули зависят друг от друга строго в одну сторону, как стрелочки:
(:app -> :features) -> :domain <- :data
:appи:featuresзнают про:domainи:core.:domainне знает НИ-О-КОМ. Он только про бизнес-логику. Чистый Kotlin/Java модуль. Тестируется элементарно.:dataзнает про:domain(чтобы реализовывать его интерфейсы) и про:core.:coreтоже ни от кого не зависит.
Получается, если завтра вы захотите заменить Room на Realm, или Retrofit на Ktor — вы идете только в модуль :data. :domain и :features даже не узнают об этом. Это и есть сила.
Бонусы (для отличников)
- Dynamic Delivery — когда хочется, чтобы приложение было меньше, а некоторые фичи качались только тем, кому надо. Тот же модуль
:feature-cat-gifsможно сделатьdynamic feature. Но это уже высший пилотаж, без реальной нужды лучше не лезть — можно и обжечься. - Multi-module navigation — навигация между фичами-модулями. Тут либо свой роутер пилить, либо использовать Navigation Component с Deep Links. Главное — не передавать туда-сюда Parcelable-модели из
:data, а только простые аргументы или ключи. Иначе связность опять поползет вверх. - Kotlin DSL для Gradle — это просто приятно.
build.gradle.ktsчитается в разы лучше, чем этот старенький Groovy с его загадочными синтаксисом. Плюс автокомплит в студии работает. Мелочь, а приятно.
Чего избегать как огня (иначе будет вам хиросима):
- God-activity/fragment. Эта срань, в которой 1000 строк кода, обработка кликов, работа с сетью, обновление UI и ещё запись в базу. Это антипаттерн, его надо выжигать калёным железом. Активность/фрагмент/компоузбл — это только отображение и передача событий во вьюмодель. ВСЁ.
- Хардкод. Строки, цвета, размеры, ключи для API — всё в ресурсы или константы в
:core."https://api.myapp.com/v1"прямо в коде ретрофита — это прямой путь в ад, когда на препроде будет другой урл. - Legacy-код без миграции. Если тянете старый проект — не надо лепить новую фичу по-старому. Выделяете для неё новый
:feature-*модуль и делаете по уму. Постепенно старый код будет вытесняться. Иначе получится франкенштейн, которого все будут бояться.
Короче, суть в чём. Чистая архитектура — это не про то, чтобы сразу написать идеальный код. Это про то, чтобы через полгода, когда придёт новая задача, ты не хватался за голову с криком «какого хуя?», а понимал, в какой именно модуль идти и что менять. Это инвестиция в своё же спокойное будущее. Ну, или в будущее того бедолаги, который будет поддерживать твой код после тебя. Не будь пидарасом шерстяным, делай хорошо сразу.