Урок по треку Архитектура мобильных приложений 7 мин

MVC, MVP, MVVM, MVI и VIPER в мобильной разработке

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

Вопрос «какую архитектуру вы используете и почему» звучит на собесах iOS, Android, Flutter и React Native. Важно не перечислить буквы, а объяснить, какую проблему решает каждый паттерн и чем вы платите.

Проблема: толстый экран

Без архитектуры весь код экрана живёт в одном классе: UIViewController, Activity/Fragment, StatefulWidget, компонент React. Там же сеть, валидация, форматирование и навигация. Такой экран трудно читать, нельзя протестировать без UI и страшно менять. Все паттерны ниже отвечают на вопрос: куда вынести логику из view.

MVC

Модель (данные), View (отображение), Controller (связывает их). В iOS UIViewController одновременно управляет view и жизненным циклом, поэтому на практике контроллер разрастается: отсюда шутка «Massive View Controller». MVC нормален для маленьких экранов, но не даёт границ.

MVP

Presenter получает события от пассивной view и командует ей через интерфейс:

interface ProfileView { fun showLoading(); fun showProfile(p: Profile); fun showError(m: String) }
class ProfilePresenter(private val repo: ProfileRepository) {
    var view: ProfileView? = null
    suspend fun load() { view?.showLoading(); ... }
}

Плюс: логика тестируется с фейковой view. Минус: двусторонняя связь, много методов интерфейса, ручное управление ссылкой на view.

MVVM

View model хранит состояние экрана и не знает о view. View наблюдает состояние и отправляет действия:

class ProfileViewModel(private val repo: ProfileRepository) : ViewModel() {
    private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val state: StateFlow<ProfileState> = _state
    fun refresh() { viewModelScope.launch { _state.value = load() } }
}

В Swift то же через @Observable, во Flutter через ChangeNotifier или Cubit, в React Native через хуки и сторы. MVVM естественно ложится на декларативные UI: SwiftUI, Compose, Flutter. Это самый частый выбор сегодня.

MVI и однонаправленный поток

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

действие → обработка → новое состояние → отрисовка

Плюсы: состояние всегда одно значение, его легко логировать, воспроизводить и тестировать. Минус: шаблонный код. Тот же принцип в TCA (iOS), BLoC (Flutter), Redux (React Native), Orbit и MVIKotlin (Android). На практике «MVVM с одним состоянием и UDF» почти неотличим от лёгкого MVI.

VIPER

Экран делится на View, Interactor (бизнес-логика), Presenter (подготовка данных для view), Entity (модели), Router (сборка и навигация). Даёт жёсткие границы для больших команд на UIKit, но каждый экран это 5+ файлов, и для простых экранов это избыточно.

Моделирование состояния

Плохо:

var isLoading = false
var error: String?
var items: [Item]?

Флаги допускают невозможные комбинации (загрузка, ошибка и данные одновременно). Лучше закрытая иерархия:

enum ScreenState {
    case loading
    case content([Item], isRefreshing: Bool)
    case empty
    case error(String)
}

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

Тост, переход, вибрация должны произойти один раз. Если хранить их флагом в состоянии, после поворота экрана или повторной подписки событие повторится. Варианты: событие как часть состояния с подтверждением обработки от view или отдельный канал (с риском потери, если подписчика нет). Навигацию лучше отдавать координатору или роутеру напрямую.

Как выбирать

  • Маленькое приложение, декларативный UI: MVVM с одним состоянием на экран.
  • Сложные экраны с множеством событий и эффектов: MVI или фреймворк вроде TCA/BLoC.
  • Большая команда на UIKit с требованием жёстких границ: VIPER или его облегчённые варианты.
  • В любом случае: один подход на проект, навигация и данные вне паттерна экрана.

Частые ошибки

  • View model держит ссылку на view или контекст Activity.
  • Набор флагов вместо одного состояния.
  • Навигация внутри экранов, которая мешает переиспользованию.
  • Разные паттерны на соседних экранах без причины.

Что спросят на собесе

  • Чем отличаются MVC, MVP, MVVM, MVI и VIPER?
  • Почему MVC в iOS называют Massive View Controller?
  • Как вы моделируете состояние и одноразовые события?
  • Какой паттерн вы выберете для нового проекта и почему?
  • Найдите проблемы в коде view model.

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

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