Архитектурные вопросы отделяют middle от senior. Интервьюеру важно не название паттерна, а понимание: куда идут данные, кто владеет состоянием, где границы слоёв и почему именно так.
Рекомендованная Google архитектура
Официальный гайд описывает слои:
- UI layer: экраны (Compose или View) и state holders (ViewModel). Показывает состояние, передаёт действия пользователя.
- Domain layer (необязательный): use case-ы с бизнес-логикой, которая переиспользуется несколькими ViewModel или слишком сложна для неё.
- Data layer: репозитории и источники данных (сеть, база, DataStore). Репозиторий единственная точка доступа к данным своего типа и решает, откуда их брать.
Зависимости направлены внутрь: UI знает о domain/data, data ничего не знает об UI. Два ключевых принципа: единый источник правды (у каждого типа данных один владелец) и однонаправленный поток данных (UDF): состояние течёт вниз, события вверх.
MVVM
class FeedViewModel(private val repo: FeedRepository) : ViewModel() {
val state: StateFlow<FeedUiState> = repo.observeFeed()
.map { FeedUiState.Content(it) as FeedUiState }
.catch { emit(FeedUiState.Error) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), FeedUiState.Loading)
fun onLike(id: String) = viewModelScope.launch { repo.like(id) }
}
View подписывается на состояние ViewModel и вызывает её методы. ViewModel не знает о View. Проблема «классического» MVVM с десятком отдельных LiveData: состояние размазано, и легко получить несогласованную комбинацию. Поэтому сейчас обычно один UiState на экран.
MVI
Model-View-Intent доводит UDF до строгости:
- Intent (действие):
sealed interface FeedAction { data object Refresh; data class Like(val id: String) }; - State: один неизменяемый объект состояния;
- Reducer: чистая функция
(State, Result) -> State; - Side effects: одноразовые события (навигация, тост) отдельным каналом.
fun onAction(action: FeedAction) {
when (action) {
FeedAction.Refresh -> refresh()
is FeedAction.Like -> like(action.id)
}
}
Плюсы: предсказуемость, весь переход состояний в одном месте, легко логировать и тестировать, хорошо сочетается с Compose. Минусы: больше шаблонного кода, риск раздутого состояния, нужно аккуратно с одноразовыми событиями. Популярные реализации в РФ-проектах: свой reducer на StateFlow, Orbit MVI, MVIKotlin.
На практике граница размыта: «MVVM с одним UiState и sealed-действиями» почти не отличается от MVI.
Одноразовые события
Навигация и снекбары плохо ложатся в состояние. Варианты:
Channel(Channel.BUFFERED)+receiveAsFlow()во ViewModel, сбор в UI сrepeatOnLifecycle. Событие не теряется без подписчика и доставляется один раз.- Событие как часть состояния (
val userMessage: String?), UI после показа вызываетonMessageShown(), ViewModel обнуляет поле. Этот вариант рекомендует гайд Android: состояние нельзя потерять.
SharedFlow без replay для событий опасен: без подписчика (во время поворота) событие пропадёт.
Clean Architecture
Идея Роберта Мартина: бизнес-логика в центре не зависит от фреймворков, БД и UI. В Android её обычно воплощают как:
domain: сущности, интерфейсы репозиториев, use case-ы; чистый Kotlin, без Android;data: реализации репозиториев, DTO, мапперы, Retrofit, Room;presentation: ViewModel, UI.
Зависимость data → domain через интерфейсы (инверсия зависимостей). Use case это класс с одной операцией: class GetFeedUseCase(private val repo: FeedRepository) { operator fun invoke() = repo.observeFeed() }.
Критика, которую стоит знать: use case-прокладки, которые только вызывают репозиторий, и тройные модели (DTO, Entity, Domain, UI) на каждый простой экран дают много кода без пользы. Хороший ответ на собесе: слой domain вводят, когда логика реально переиспользуется или сложна.
Модульность
В больших проектах модули по фичам (:feature:feed, :feature:profile) плюс общие (:core:network, :core:database, :core:ui). Плюсы: параллельная и инкрементальная сборка, изоляция команд, явные границы. Фичи не зависят друг от друга напрямую, навигация через API-модули или общий контракт.
Частые ошибки
- ViewModel, которая знает о View, Context или навигационном контроллере.
- Несколько несогласованных источников состояния для одного экрана.
- Бизнес-логика в composable или Activity.
- Use case на каждую строку без пользы и бесконечные мапперы.
- События через
StateFlowилиSharedFlowбез replay.
Что спросят на собесе
- Чем MVVM отличается от MVI? Что выберете и почему?
- Что такое UDF и единый источник правды?
- Какие слои у Clean Architecture и как направлены зависимости?
- Когда нужен use case, а когда это лишняя прослойка?
- Как обрабатывать одноразовые события?
- Как бы вы разбили приложение на модули?