Изучение КММ: запись 3

Итак, как я уже говорил в статье Learning KMM: Entry 1…

чтобы заставить себя не бросать этот проект, я попытаюсь написать о своем опыте изучения Kotlin Multiplatform Mobile.

Я не забыл об этом, просто был немного ленив занят 😅.


🥱 Сразу предупреждаю, что эта статья будет немного длиннее, чем первые две, и я разобью ее на две части.

⌛️ TLDR: посмотрите репозиторий и почитайте документацию по MVI Kotlin и декомпозиции здесь.


📸 Обзор

Итак, для начала, эта статья будет посвящена созданию многократно используемых компонентов и классов, которые содержат бизнес-логику для входа в мои barebone представления.

Для этого я использовал следующие пакеты Kotlin

  • MVI Kotlin
  • Essenty
  • Napier

Архитектура MVI очень нова для меня, и я могу объяснять это немного по-идиотски, но, тем не менее, я буду стараться изо всех сил.


⚔️ MVI vs MVVM

MVI расшифровывается как Model-View-Intent и является..:

архитектурный паттерн, использующий однонаправленный поток данных.

Это означает, что данные движутся только в одном направлении от модели к представлению и наоборот. Этот поток помогает поддерживать единый источник истины и обычно содержит единое неизменяемое состояние всего представления. Он, как и MVVM, обычно позволяет модели ничего не знать о представлении.

MVVM расшифровывается как Model-View-ViewModel и является..:

структурирован для разделения программной логики и элементов управления пользовательского интерфейса.

MVVM — это то, с чем я более знаком. Этот паттерн обычно не подразумевает наличие всего неизменяемого состояния представления внутри компонента, но, как уже говорилось, модель представления совершенно не знает о представлении.

Из беглого чтения я знаю, что некоторые предпочитают MVVM MVI, так как в нем меньше компонентов и классов и его легче «запустить», чтобы подключить представления к компонентам бизнес-логики или моделям представления.

Также из беглого исследования, кажется, что есть два популярных пакета/библиотеки, которые обычно используются для MVI и MVVM в мультиплатформенных мобильных проектах на Kotlin.

  • MVI Kotlin by arkivanov
  • moko mvvm от icerock development

Я выбираю MVI Kotlin только потому, что я ничего не знаю о нем, так что это будет большим опытом обучения.


🏗 Установка

Итак, есть несколько необходимых пакетов, которые нужно добавить в общий файл gradle для начала работы.

listOf(
    iosX64(),
    iosArm64(),
    iosSimulatorArm64()
).forEach {
    it.binaries.framework {
        baseName = "Shared"
        export("com.arkivanov.decompose:decompose:0.6.0")
        export("com.arkivanov.essenty:lifecycle:0.4.1")
        export("com.arkivanov.mvikotlin:mvikotlin-main:3.0.0-beta02")
    }
}

commonMain {
    dependencies {
        implementation("com.arkivanov.mvikotlin:mvikotlin:3.0.0-beta02")
        implementation("com.arkivanov.mvikotlin:rx:3.0.0-beta02")
        implementation("com.arkivanov.mvikotlin:mvikotlin-main:3.0.0-beta02")
        implementation("com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:3.0.0-beta02")
        implementation("com.arkivanov.decompose:decompose:0.6.0")
        implementation("com.arkivanov.essenty:lifecycle:0.4.1")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
        implementation("io.github.aakira:napier:2.6.1")
    }
}

named("iosMain") {
    dependencies {
        api("com.arkivanov.mvikotlin:mvikotlin:3.0.0-beta02")
        api("com.arkivanov.mvikotlin:rx:3.0.0-beta02")
        api("com.arkivanov.mvikotlin:mvikotlin-main:3.0.0-beta02")
        api("com.arkivanov.mvikotlin:mvikotlin-extensions-coroutines:3.0.0-beta02")
        api("com.arkivanov.decompose:decompose:0.6.0")
        api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
        api("com.arkivanov.essenty:lifecycle:0.4.1")
        api("io.github.aakira:napier:2.6.1")
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь есть несколько довольно специфических моментов, на которые следует обратить внимание, и с которыми я столкнулся. Во-первых, вам нужно убедиться, что зависимости iOS помечены как «api», а не «implementation», и, если честно, я не знаю, почему 😅. Во-вторых, важно убедиться, что вы экспортировали эти три библиотеки в ваш общий iOS-фреймворк, чтобы вы могли использовать их в своем Swift-коде. В проекте Android вы можете обойтись добавлением этих зависимостей прямо в файл gradle. Как только вы все это настроите, вы должны быть готовы к работе 🎲.


Создание модели 🎨

Первая часть MVI — это модель, вы не можете иметь MVI без модели!

В данном случае модель на самом деле очень проста для экрана входа в систему с двумя входами.

data class Model(
    val username: String,
    val password: String
)
Вход в полноэкранный режим Выход из полноэкранного режима

🏪 Магазин

Хранилище — это место, где будет находиться бизнес-логика, которое будет принимать все «намерения» и «сообщения» и выводить все «метки» и «состояния».

В пакете MVI Kotlin вы можете выбрать расширения Reaktive или Coroutine и создать красивое хранилище. Я выбрал Coroutines из личных предпочтений и опыта.

Сначала для нашего магазина нам нужно определить состояние.

internal data class State(
    val username: String = "",
    val password: String = ""
)
Войти в полноэкранный режим Выход из полноэкранного режима

Необходимо определить «Намерения» магазина. Мне нравится думать о «намерениях» как о действиях, которые «намереваются изменить состояние». В данном случае их три.

  1. Одно для обновления имени пользователя
  2. Одно для обновления пароля
  3. Одно обновление для попытки входа в систему
internal sealed interface Intent {
    data class UpdateUsername(val username: String): Intent
    data class UpdatePassword(val password: String): Intent
    data class Login(val username: String, val password: String): Intent
}
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, когда «Намерение» отправляется в магазин, оно выполняется «Исполнителем», который вы увидите в ближайшее время. Когда «Намерение» выполняется, оно выводит «Сообщение». Это «Сообщение» содержит все, что произошло в результате выполнения «Намерения». Выходной сигнал, или «Сообщение», затем отправляется в «Редуктор», который уменьшает состояние, используя «Сообщение». Это звучит очень запутанно, но все становится немного понятнее, когда вы видите реализацию.

Сначала «Магазин» во всей своей красе ⭐️

@OptIn(ExperimentalMviKotlinApi::class)
internal fun loginStore(storeFactory: StoreFactory) : Store<Intent, State, Nothing> =
    storeFactory.create<Intent, Nothing, Msg, State, Nothing>(
        name = "LoginStore",
        initialState = State(),
        executorFactory = coroutineExecutorFactory {
            onIntent<Intent.Login> { intent ->
                val token = logIn(intent.username, intent.password)
                dispatch(Msg.LoggedIn(token))
            }
            onIntent<Intent.UpdateUsername> { dispatch(Msg.UpdateUsername(it.username)) }
            onIntent<Intent.UpdatePassword> { dispatch(Msg.UpdatePassword(it.password)) }
        },
        reducer = { msg ->
            when (msg) {
                is Msg.LoggedIn -> copy()
                is Msg.UpdatePassword -> copy(password = msg.password)
                is Msg.UpdateUsername -> copy(username = msg.username)
            }
        }
    )

private fun logIn(username: String, password:String) : String {
    // Authenticate the user
    // this is probably going to take a while
    // this will also return a auth token
    Napier.i("called log in $username, $password")
    return "AuthToken"
}
Вход в полноэкранный режим Выход из полноэкранного режима

А теперь «Сообщения» 📨.

private sealed interface Msg {
    class UpdateUsername(val username: String): Msg
    class UpdatePassword(val password: String): Msg
    class LoggedIn(authToken: String) : Msg
}
Войти в полноэкранный режим Выход из полноэкранного режима

Окончательное разбиение:
Возможно, более простой способ объяснить — это просто показать, как кто-то пытается войти в систему.

  1. Пользователь загружает экран входа в систему с состоянием по умолчанию нового класса данных Login (имя пользователя и пароль в пустой строке).
  2. Пользователь набирает имя пользователя, и каждое нажатие клавиши посылает «Intent» исполнителю.
  3. Поскольку при изменении имени пользователя нет никакой логики, мы немедленно отправляем «Сообщение», содержащее последнюю строку имени пользователя.
  4. Редуктор» подхватывает новое имя пользователя и КОПИРУЕТ (подчеркивание — копирует) весь объект состояния с новым именем пользователя.
  5. Те же шаги (2-4) выполняются с паролем.
  6. После того, как пользователь ввел имя пользователя и пароль, он может нажать кнопку для входа в систему.
  7. В исполнителе для «входа» вы можете увидеть, что мы имитируем некоторый тип бизнес-логики, которая попадает в механизм Auth для проверки имени пользователя и пароля и возвращает «токен Auth».
  8. После возврата токена мы отправляем сообщение, содержащее токен, который затем может быть скопирован в состояние или, надеюсь, в будущем сохранен локально.

Поскольку мы на самом деле не подключили наш аутентификатор (собираемся использовать Firebase Auth в следующем посте, надеюсь 🤞), я добавил некоторые логи, чтобы просто перепроверить, что отправляется в функцию входа.

Надеюсь, это объяснение имеет для вас некоторый смысл 🙂 .


И здесь я собираюсь перейти ко второй (технически 4-й) части серии, чтобы поговорить о том, как подключить магазин входа к представлению.

Оставайтесь с нами до выхода 4 части и, как всегда, вы можете просмотреть весь код здесь, в репозитории Voix GitHub.

Оцените статью
devanswers.ru
Добавить комментарий