Нативный 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 только отображает.