Kotlin Multiplatform Mobile (KMM): пример простого приложения
Пример простого приложения (счетчика) под обе мобильные платформы, использующего общий код на языке Kotlin. Если вы задумываетесь над созданием мобильного приложения одновременно под iOS и Android, знакомы с языком Kotlin и хотите попробовать что-то новенькое, то обратите внимание на Kotlin Multiplatform Mobile (KMM). Это SDK (набор инструментов для разработки программного обеспечения) разработанный компанией JetBrains (создателя языка Kotlin), недавно вышедший в публичную бету, а значит, самое время попробовать его в деле! В этой статье мы разберем: Ядром KMM является технология Kotlin Native, позволяющая компилировать код, написанный на языке Kotlin, в платформонезависимые, нативные приложения и библиотеки. Самое важное отличие от популярных решений для мультиплатформенной разработки, таких как React Native или Flutter, это то, что KMM предоставляет набор инструментов, позволяющий использовать общую логику для обоих приложений в виде отдельной библиотеки, написанной на языке Kotlin, которую затем можно импортировать как в Android, так и в iOS приложения (рис. 1). Рис.1. Архитектура KMM-приложения Основные плюсы такого подхода: Рис. 2. Использование общего кода между серверной логикой, мобильными платформами и web Но не стоит забывать и о минусах: Команда JetBrains активно работает над развитием комьюнити, а также улучшает плагин для Android Studio, который заметно упрощает создание и поддержку KMM приложения. А последний минус мы рассмотрим подробнее далее и разберем популярный способ организовать код приложения так, чтобы максимально следовать золотому правилу разработки – DRY (don’t repeat yourself). Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека мобильного разработчика» Интересно, перейти к каналу Архитектура типичного мультиплатформенного приложения – это монорепозиторий, состоящий из трех модулей: Android-приложение, iOS-приложение и общая библиотека (рис. 3). Рис. 3. Схема модулей базового KMM приложения Проект с такой структурой можно создать с помощью официального плагина для Android Studio. Шаги по настройке подробно описаны на сайте проекта, поэтому мы не будем углубляться в него подробнее. Теперь предстоит начать писать общий код. Но как же это сделать? Ведь часто много логики находится непосредственно в UI-компонентах, а что делать с навигацией? Здесь нам на помощь приходит архитектурный паттерн из Flutter, предложенный его создателями компанией Google в 2018. Этот паттерн называется BLoC (business-logic components) и он предлагает вынести всю бизнес-логику из UI-компонента в специальный компонент – BLoC (рис. 4). Рис. 4. Визуализация иерархии BLoC‘ов Такие bloc’и тесно связаны с определенным UI-компонентом и его жизненным циклом, они создаются и уничтожаются вместе с ним. Но bloc’и не зависят от его реализации и не содержат никакого UI, а значит, они идеальные кандидаты для помещения в общий код. Библиотеки, позволяющие легко внедрить данный подход, уже созданы и активно поддерживаются комьюнити. Самая популярная из них на GitHub – Decompose, на ней мы и остановимся чуть подробнее. В качестве примера рассмотрим простейший счетчик, который умеет показывать текущее значение и который можно увеличивать и уменьшать на единицу. Опишем бизнес-логику нашего компонента: Создадим Kotlin-интерфейс с этой логикой в общем модуле: CounterComponent.kt Это и есть интерфейс нашего bloc’а. Примечание Мы используем Напишем реализацию для нашего bloc’а: CounterComponent.kt Теперь наш bloc готов к использованию в iOS и Android-приложениях. Для Android-приложения создадим простой компонент на Jetpack Compose (для простоты восприятия, все модификаторы были убраны из кода): CounterUi.kt Для простоты, наш счетчик – единственный компонент приложения, поэтому мы можем создать наш bloc в MainActivity.kt Перейдем к iOS-приложению на SwiftUI: CounterView.swift Код не сильно отличается от того, что был в Android: для отслеживания изменений Value используется специальный адаптер – Создаем bloc также при инициализации приложения: iOSApp.swift Наше KMM приложение-счетчик готово. Вся бизнес-логика находится в общем модуле и используется совместно iOS- и Android-частями. Код на Гитхабе Код получившегося приложения можно скачать в репозитории. В статье мы коснулись только самого базового функционала библиотеки. Кроме него, Decompose содержит инструменты для навигации между экранами, сохранения состояния и обработки нажатия кнопки Back для Android Евгений Хохлов Tech Lead в компании Chatfuel Что особенного в KMM и чем он отличается от других технологий мультиплатформенной разработки?
Архитектура типичного KMM-приложения
Давайте разберем пример создания BLoC’а в библиотеке Decompose
data class CounterState(val count: Int) interface CounterComponent { val state: Value<CounterState> // (1) fun onIncrease() // (2) fun onDecrease() // (3) }
Value
– это специальный интерфейс, который представляет библиотека Decompose для хранения состояния компонента, который интегрируется со SwiftUI и Jetpack Compose (нативными библиотеками iOS и Android для UI).CounterState
, а не просто Int
, так как из-за особенностей Kotlin Native, Value
не может хранить значение примитивного типа.
class DefaultCounterComponent: CounterComponent { override val state = MutableValue(CounterState(0)) override fun onIncrease() { state.reduce { it.copy(count = it.count + 1) } } override fun onDecrease() { state.reduce { it.copy(count = it.count - 1) } } }
MutableValue
реализует интерфейс Value
и также предоставляется Decompose. reduce
– еще один хэлпер, который позволяет обновить текущее значение на основе предыдущего.
@Composable fun CounterUi(counterComponent: CounterComponent) { val state by counterComponent.state.subscribeAsState() Column { Text(text = "${state.count}") Button(onClick = counterComponent::onIncrease) { Text(text = "+") } Button(onClick = counterComponent::onDecrease) { Text(text = "-") } } }
subsribeAsState()
– расширение, которое позволяет трансформировать Value
в State
, также предоставляется Decompose MainActivity
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val counterComponent = DefaultCounterComponent() setContent { CounterUi(counterComponent) } } }
import SwiftUI import shared // наша общая библиотека struct CounterView: View { private let component: CounterComponent @ObservedObject private var state: ObservableValue<CounterState> init(_ component: CounterComponent) { self.component = component state = ObservableValue<CounterState>(component.state) } var body: some View { HStack { Text("(state.value.count)") Button(action: { component.onIncrease() }) { Text("+") } Button(action: { component.onDecrease() }) { Text("-") } } } }
ObservableValue
, код которого можно скопировать из официального репозитория Decompose.
@main struct iOSApp: App { var counterComponent = DefaultCounterComponent() var body: some Scene { WindowGroup { CounterView(counterComponent) } } }
Что еще предоставляет Decompose?
Полезные ресурсы для изучения Kotlin Multiplatform Mobile
- 0 views
- 0 Comment