khokhlov.e.n 20 декабря 2022

🤖🍏 Android + iOS: организация кода приложения с использованием Kotlin Multiplatform Mobile

Пример простого приложения (счетчика) под обе мобильные платформы, использующего общий код на языке Kotlin.
🤖🍏 Android + iOS: организация кода приложения с использованием Kotlin Multiplatform Mobile

Если вы задумываетесь над созданием мобильного приложения одновременно под 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-приложения
Рис.1. Архитектура KMM-приложения

Основные плюсы такого подхода:

  • Легко добавить в существующие нативные приложения. Достаточно просто постепенно выделять общий код в отдельную библиотеку.
  • Меньший размер приложения. Компиляция кода Kotlin Native добавляет небольшой оверхед, поэтому размер приложения обычно меньше аналогов на Flutter и React Native.
  • Большая гибкость. Вы можете использовать любые библиотеки и технологии внутри ваших iOS и Android-приложения, без необходимости написания дополнительного кода.
  • Общий код можно использовать не только в мобильных приложениях, но также и в веб-приложениях, и в серверной логике на Kotlin (рис. 2).
Рис. 2. Использование общего кода между серверной логикой, мобильными платформами и web
Рис. 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 приложения
Рис. 3. Схема модулей базового KMM приложения

Проект с такой структурой можно создать с помощью официального плагина для Android Studio. Шаги по настройке подробно описаны на сайте проекта, поэтому мы не будем углубляться в него подробнее.

Теперь предстоит начать писать общий код. Но как же это сделать? Ведь часто много логики находится непосредственно в UI-компонентах, а что делать с навигацией? Здесь нам на помощь приходит архитектурный паттерн из Flutter, предложенный его создателями компанией Google в 2018. Этот паттерн называется BLoC (business-logic components) и он предлагает вынести всю бизнес-логику из UI-компонента в специальный компонент – BLoC (рис. 4).

Рис. 4. Визуализация иерархии <b>BLoC</b>'ов
Рис. 4. Визуализация иерархии BLoC'ов

Такие bloc’и тесно связаны с определенным UI-компонентом и его жизненным циклом, они создаются и уничтожаются вместе с ним. Но bloc’и не зависят от его реализации и не содержат никакого UI, а значит, они идеальные кандидаты для помещения в общий код. Библиотеки, позволяющие легко внедрить данный подход, уже созданы и активно поддерживаются комьюнити. Самая популярная из них на GitHub – Decompose, на ней мы и остановимся чуть подробнее.

Давайте разберем пример создания BLoC’а в библиотеке Decompose

В качестве примера рассмотрим простейший счетчик, который умеет показывать текущее значение и который можно увеличивать и уменьшать на единицу.

Опишем бизнес-логику нашего компонента:

  1. Он хранит состояние – текущее значение счетчика.
  2. У него есть метод для увеличения значения на один.
  3. У него есть метод уменьшения значения на один.

Создадим 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

  1. Официальный get-started от компании JetBrains
  2. Страница с примерами приложений на KMM
  3. Документация Decompose
  4. Материалы от компании IceRock
  5. Поиск библиотек для KMM

Евгений Хохлов Tech Lead в компании Chatfuel

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ