Урок по треку Android 8 мин

Архитектура: MVVM, MVI и Clean за 8 минут

Слои, UDF, UiState, события, use case, репозиторий и модульность.

Архитектурные вопросы отделяют 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.

Одноразовые события

Навигация и снекбары плохо ложатся в состояние. Варианты:

  1. Channel(Channel.BUFFERED) + receiveAsFlow() во ViewModel, сбор в UI с repeatOnLifecycle. Событие не теряется без подписчика и доставляется один раз.
  2. Событие как часть состояния (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, а когда это лишняя прослойка?
  • Как обрабатывать одноразовые события?
  • Как бы вы разбили приложение на модули?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.