Нативный UI поверх общего модуля

Классический KMP: интерфейс на Compose и SwiftUI, общая логика — под ним.

Нативный UI поверх KMP — подход, при котором экраны пишутся на Jetpack Compose (Android) и SwiftUI/UIKit (iOS), а общий модуль предоставляет им состояние и обработчики действий.

Как UI подключается к общему коду

В классическом KMP UI — тонкий: он подписывается на состояние из shared ViewModel и шлёт обратно действия. Никакой бизнес-логики в UI нет. На Android это выглядит привычно для Compose-разработчика:

// androidMain или модуль Android-приложения
@Composable
fun OrderListScreen(viewModel: OrderListViewModel) {
    val state by viewModel.state.collectAsState()
    when (state) {
        is OrderListState.Loading -> Spinner()
        is OrderListState.Data -> OrderList((state as OrderListState.Data).items)
        OrderListState.Error -> ErrorView()
    }
}

То же самое на стороне iOS

На iOS SwiftUI читает то же состояние из того же ViewModel (через обёртку над StateFlow):

struct OrderListScreen: View {
    @StateObject var observable: OrderListObservable // обёртка над KMP ViewModel
    var body: some View {
        switch observable.state {
        case is OrderListState.Loading: ProgressView()
        case let data as OrderListState.Data: OrderList(items: data.items)
        default: ErrorView()
        }
    }
}

Два разных UI, одна логика, один источник состояния.

Преимущество нативного UI

Каждый экран — настоящий компонент платформы. SwiftUI-список получает все системные жесты, accessibility, Dynamic Type, тёмную тему «бесплатно». Когда Apple добавит новый стиль навигации, он появится у вас без обновления фреймворка. То же на Android с Material. Это главная причина выбирать классический KMP вместо Flutter, если родной look-and-feel критичен.

Как работает под капотом

UI и общий модуль связаны через состояние, а не вызовы напрямую. UI подписывается на StateFlow; когда ViewModel меняет состояние, поток уведомляет подписчика, и платформенный фреймворк перерисовывает экран своими средствами. На Android это рекомпозиция Compose, на iOS — обновление SwiftUI по @Published/ObservableObject в обёртке. Общий код ничего не знает о том, как именно происходит перерисовка — он лишь публикует новое состояние.

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

Дублировать в UI логику, которая должна быть в ViewModel (например, фильтрацию списка) — тогда теряется смысл общего кода. UI только отображает. Вторая ошибка — забыть про обёртку для iOS и пытаться напрямую жонглировать StateFlow в SwiftUI, получая нечитаемый код. Третья — считать, что раз UI разный, общего выигрыша мало; на деле presentation и ниже — это большая часть кода.

Итоги

  • UI тонкий: подписывается на состояние и шлёт действия в общий ViewModel.
  • Compose и SwiftUI работают с одним источником состояния.
  • Нативный UI бесплатно получает системные возможности платформы.
  • Вся логика — в общем коде, UI только отображает.
Проверьте себя
1. Какова роль UI в классическом KMP?
AСодержит бизнес-логику
BТонкий слой: подписывается на состояние и отправляет действия в общий ViewModel
CДелает сетевые запросы напрямую
DХранит данные
2. Главное преимущество нативного UI над Flutter?
AМеньше кода
BЭкраны — настоящие компоненты платформы и получают системные возможности бесплатно
CНе нужен Kotlin
DБыстрее компиляция