Пример простого приложения (счетчика) под обе мобильные платформы, использующего общий код на языке Kotlin. Если вы задумываетесь над созданием мобильного приложения одновременно под iOS и Android, знакомы с языком Kotlin и хотите попробовать что-то новенькое, то обратите внимание на Kotlin Multiplatform Mobile (KMM). Это SDK (набор инструментов для разработки программного обеспечения) разработанный компанией JetBrains (создателя языка Kotlin), недавно вышедший в публичную бету, а значит, самое время попробовать его в деле! В этой статье мы разберем: основные особенности KMM; архитектуру типичного KMM-приложения; приемы организации кода; ресурсы для дальнейшего изучения. Что особенного в KMM и чем он отличается от других технологий мультиплатформенной разработки? Ядром KMM является технология Kotlin Native, позволяющая компилировать код, написанный на языке Kotlin, в платформонезависимые, нативные приложения и библиотеки. Самое важное отличие от популярных решений для мультиплатформенной разработки, таких как React Native или Flutter, это то, что KMM предоставляет набор инструментов, позволяющий использовать общую логику для обоих приложений в виде отдельной библиотеки, написанной на языке Kotlin, которую затем можно импортировать как в Android, так и в iOS приложения (рис. 1). Рис.1. Архитектура KMM-приложения Основные плюсы такого подхода: Легко добавить в существующие нативные приложения. Достаточно просто постепенно выделять общий код в отдельную библиотеку. Меньший размер приложения. Компиляция кода Kotlin Native добавляет небольшой оверхед, поэтому размер приложения обычно меньше аналогов на Flutter и React Native. Большая гибкость. Вы можете использовать любые библиотеки и технологии внутри ваших iOS и Android-приложения, без необходимости написания дополнительного кода. Общий код можно использовать не только в мобильных приложениях, но также и в веб-приложениях, и в серверной логике на Kotlin (рис. 2). Рис. 2. Использование общего кода между серверной логикой, мобильными платформами и web Но не стоит забывать и о минусах: Высокий порог входа. Необходимы базовые знания разработки под iOS и Android даже для создания простого приложения. Небольшое комьюнити. Так как KMM все еще находится в бете, его использует не такое большое число пользователей. Для сравнения: на момент написания статьи вопросов с тегом Flutter в Stackoverflow больше 160 тысяч, а по KMM – чуть больше тысячи. Небольшое количество мультиплатформенных библиотек. Несмотря на то что стандартная библиотека языка Kotlin может быть использована в общем коде без ограничений, многие привычные библиотеки языка Kotlin используют платформозависимые вызовы Java-кода и не могут быть использованы напрямую. Сложность организации общего кода. KMM не предоставляет никаких готовых решений, как организовать архитектуру приложения, чтобы увеличить процент общего кода и не усложнить процесс разработки. Команда JetBrains активно работает над развитием комьюнити, а также улучшает плагин для Android Studio, который заметно упрощает создание и поддержку KMM приложения. А последний минус мы рассмотрим подробнее далее и разберем популярный способ организовать код приложения так, чтобы максимально следовать золотому правилу разработки – DRY (don’t repeat yourself). Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека мобильного разработчика» Интересно, перейти к каналу Архитектура типичного KMM-приложения Архитектура типичного мультиплатформенного приложения – это монорепозиторий, состоящий из трех модулей: 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, на ней мы и остановимся чуть подробнее. Давайте разберем пример создания BLoC’а в библиотеке Decompose В качестве примера рассмотрим простейший счетчик, который умеет показывать текущее значение и который можно увеличивать и уменьшать на единицу. Опишем бизнес-логику нашего компонента: Он хранит состояние – текущее значение счетчика. У него есть метод для увеличения значения на один. У него есть метод уменьшения значения на один. Создадим Kotlin-интерфейс с этой логикой в общем модуле: CounterComponent.kt data class CounterState(val count: Int) interface CounterComponent { val state: Value<CounterState> // (1) fun onIncrease() // (2) fun onDecrease() // (3) } Это и есть интерфейс нашего bloc’а. Value – это специальный интерфейс, который представляет библиотека Decompose для хранения состояния компонента, который интегрируется со SwiftUI и Jetpack Compose (нативными библиотеками iOS и Android для UI). Примечание Мы используем CounterState, а не просто Int, так как из-за особенностей Kotlin Native, Value не может хранить значение примитивного типа. Напишем реализацию для нашего bloc’а: CounterComponent.kt 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 – еще один хэлпер, который позволяет обновить текущее значение на основе предыдущего. Теперь наш bloc готов к использованию в iOS и Android-приложениях. Для Android-приложения создадим простой компонент на Jetpack Compose (для простоты восприятия, все модификаторы были убраны из кода): CounterUi.kt @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 Для простоты, наш счетчик – единственный компонент приложения, поэтому мы можем создать наш bloc в MainActivity MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val counterComponent = DefaultCounterComponent() setContent { CounterUi(counterComponent) } } } Перейдем к iOS-приложению на SwiftUI: CounterView.swift 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("-") } } } } Код не сильно отличается от того, что был в Android: для отслеживания изменений Value используется специальный адаптер – ObservableValue, код которого можно скопировать из официального репозитория Decompose. Создаем bloc также при инициализации приложения: iOSApp.swift @main struct iOSApp: App { var counterComponent = DefaultCounterComponent() var body: some Scene { WindowGroup { CounterView(counterComponent) } } } Наше KMM приложение-счетчик готово. Вся бизнес-логика находится в общем модуле и используется совместно iOS- и Android-частями. Код на Гитхабе Код получившегося приложения можно скачать в репозитории. Что еще предоставляет Decompose? В статье мы коснулись только самого базового функционала библиотеки. Кроме него, Decompose содержит инструменты для навигации между экранами, сохранения состояния и обработки нажатия кнопки Back для Android Полезные ресурсы для изучения Kotlin Multiplatform Mobile Официальный get-started от компании JetBrains Страница с примерами приложений на KMM Документация Decompose Материалы от компании IceRock Поиск библиотек для KMM Евгений Хохлов Tech Lead в компании Chatfuel
Если вы задумываетесь над созданием мобильного приложения одновременно под 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
data class CounterState(val count: Int) interface CounterComponent { val state: Value<CounterState> // (1) fun onIncrease() // (2) fun onDecrease() // (3) }
Это и есть интерфейс нашего bloc’а. Value – это специальный интерфейс, который представляет библиотека Decompose для хранения состояния компонента, который интегрируется со SwiftUI и Jetpack Compose (нативными библиотеками iOS и Android для UI).
Value
Примечание Мы используем CounterState, а не просто Int, так как из-за особенностей Kotlin Native, Value не может хранить значение примитивного типа.
CounterState
Int
Напишем реализацию для нашего bloc’а:
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 – еще один хэлпер, который позволяет обновить текущее значение на основе предыдущего.
MutableValue
reduce
Теперь наш bloc готов к использованию в iOS и Android-приложениях.
Для Android-приложения создадим простой компонент на Jetpack Compose (для простоты восприятия, все модификаторы были убраны из кода):
CounterUi.kt
@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
subsribeAsState()
State
Для простоты, наш счетчик – единственный компонент приложения, поэтому мы можем создать наш bloc в MainActivity
MainActivity
MainActivity.kt
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val counterComponent = DefaultCounterComponent() setContent { CounterUi(counterComponent) } } }
Перейдем к iOS-приложению на SwiftUI:
CounterView.swift
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("-") } } } }
Код не сильно отличается от того, что был в Android: для отслеживания изменений Value используется специальный адаптер – ObservableValue, код которого можно скопировать из официального репозитория Decompose.
ObservableValue
Создаем bloc также при инициализации приложения:
iOSApp.swift
@main struct iOSApp: App { var counterComponent = DefaultCounterComponent() var body: some Scene { WindowGroup { CounterView(counterComponent) } } }
Наше KMM приложение-счетчик готово. Вся бизнес-логика находится в общем модуле и используется совместно iOS- и Android-частями.
Код на Гитхабе Код получившегося приложения можно скачать в репозитории.
В статье мы коснулись только самого базового функционала библиотеки. Кроме него, Decompose содержит инструменты для навигации между экранами, сохранения состояния и обработки нажатия кнопки Back для Android
Евгений Хохлов Tech Lead в компании Chatfuel
Ваш адрес email не будет опубликован. Обязательные поля помечены *
Сохранить моё имя, email и адрес сайта в этом браузере для последующих моих комментариев.
Δ
Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.