Изучение Kotlin Multiplatfom Mobile: Вступление 3.5

Это продолжение предыдущего поста Learning KMM: Entry 3.

Вместо версии 4, я использовал свой большой мозг разработчика и увеличил номер этой версии до 3.5, потому что она не очень похожа на версию 4.


📝 Введение

В прошлом посте мы рассмотрели часть MVI, посвященную модели и намерениям. В этом посте мы рассмотрим часть View.

В предыдущих постах этой серии я уже сделал два основных экрана входа в систему, используя Jetpack Compose и SwiftUI.

Этот пост будет посвящен использованию MVI Kotlin в тандеме с Decompose для создания компонентов, которые я могу подключить к моим представлениям.

🎶 Decompose

Итак, что же такое Decompose и почему мне нужно его использовать? Вам не обязательно использовать Decompose для использования созданных хранилищ в MVI Kotlin, если вы используете декларативный UI, как Jetpack Compose и SwiftUI. Однако Decompose облегчает создание компонентов с учетом жизненного цикла (что очень полезно для android) и поставляется с многоплатформенным маршрутизатором для навигации.

Благодаря этому вы теперь можете разделять еще больше кода между приложениями (логика навигации) и лучше разделять проблемы между UI и не-UI кодом.

Не стесняйтесь читать больше об обзоре Decompose здесь.

Корневой компонент 🌳

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

Корневой компонент также является местом, где будет располагаться общая навигация маршрутизатора. Внутри корневого компонента можно найти конфигурации для дочерних компонентов, а также текущее состояние маршрутизатора приложения.

Я собираюсь немного упустить детали реализации корневого компонента, чтобы сосредоточиться на представлении Login. Возможно, я вернусь к этому позже, когда буду говорить о навигации внутри приложения.

Компонент входа 🤖

После того, как корневой компонент создан, мы можем продолжить подключение нашего представления входа к магазину входа.

Поскольку мы определим начальный компонент корневого компонента как компонент входа, мы будем автоматически перенаправляться на этот вид входа при загрузке приложения.

Android

@OptIn(com.arkivanov.decompose.ExperimentalDecomposeApi::class)
@Composable
fun RootContent(component: Root) {
    Children(routerState = component.routerState) {
        when (val child = it.instance) {
            is Root.Child.Login -> LoginScreen(child.component)
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

iOS

struct RootView: View {
    @ObservedObject
        private var routerStates: ObservableValue<RouterState<AnyObject, RootChild>>

    init(_ component: Root) {
        self.routerStates = ObservableValue(component.routerState)
    }

    var body: some View {
        let child = self.routerStates.value.activeChild.instance

        switch child {

            case let login as RootChild.Login:
                LoginView(login.component)

            default: EmptyView()
        }
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

В обоих этих логических блоках вы можете видеть, что мы переключаем конкретный тип конфигурации «Child» на LoginView или LoginScreen и передаем дочерний компонент, который является нашим «LoginComponent».

Вы можете увидеть этот компонент входа в систему здесь

class LoginComponent(
    componentContext: ComponentContext,
    storeFactory: StoreFactory
) : Login, ComponentContext by componentContext {

    private val store = instanceKeeper.getStore {
        loginStore(storeFactory)
    }

    override val models: Value<Login.Model> = store.asValue().map(stateToModel)

    override fun onLogin() =
        store.accept(Intent.Login(models.value.username, models.value.password))

    override fun onPasswordChanged(password: String) =
        store.accept(Intent.UpdatePassword(password))

    override fun onUsernameChanged(username: String) =
        store.accept(Intent.UpdateUsername(username))
}
Вход в полноэкранный режим Выход из полноэкранного режима

По сути, этот класс пересылает действия, вызываемые представлением, в Login Store из предыдущего поста. Модель Login Store также раскрывается таким образом, чтобы ее можно было использовать как в Jetpack Compose, так и в SwiftUI.

🎶 Код Compose

Потребление магазина очень простое с помощью Jetpack Compose и выглядит следующим образом.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(component: Login)
{
    val state by component.models.subscribeAsState()

    // omitting some code for brevity
    ...

    OutlinedTextField(
        value = state.username,
        onValueChange = component::onUsernameChanged
    )
}
Вход в полноэкранный режим Выход из полноэкранного режима

🐣 Код SwiftUI

Потребление магазина в SwiftUI немного сложнее, чем в android. Он включает в себя создание довольно большого количества кода расширения для обертывания значения наблюдаемого, состояния маршрутизатора компонента, а также экспонирования значений библиотек MVI Kotlin и Essenty, что мы выполнили с помощью gradle в первой части этого поста. После установки всех вспомогательных расширений у вас получится что-то вроде этого:

struct LoginView: View {
    private var component: Login

    @ObservedObject
    private var models: ObservableValue<LoginModel>

    @State private var isSecured: Bool = true

    init(_ component: Login) {
        self.component = component
        self.models = ObservableValue(component.models)
    }


    var body: some View {
        let model = models.value

        let usernameBinding = Binding(get: { model.username }, set: component.onUsernameChanged)
        let passwordBinding = Binding(get: { model.password }, set: component.onPasswordChanged)

        // omitting some code for brevity
        ...

        TextField("Username", text: usernameBinding)
        Button("Login", action: component.onLogin)
}
Вход в полноэкранный режим Выход из полноэкранного режима

🧪 Тестирование с Napier

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

После некоторых исследований я наткнулся на Napier, и он оказался очень простым в настройке.

Следуя документации и настроив вызовы инициализации, я добавил ведение журнала в функцию кнопки входа.

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"
}
Вход в полноэкранный режим Выход из полноэкранного режима

После запуска приложения android и фиктивного входа в систему вы можете увидеть в журналах следующее:

2022-08-29 21:31:02.520  5005-5005  LoginStoreKt$logIn      com.belzsoftware.voix.android        I  called log in test@test.com, test123
2022-08-29 21:31:35.669  5005-5005  LoginStoreKt$logIn      com.belzsoftware.voix.android        I  called log in yhskhkjsdf, yolo7383
Вход в полноэкранный режим Выход из полноэкранного режима

И после запуска приложения для iOS:

08-09 20:58:28.603 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, sdfsdfsdf
08-09 20:58:28.765 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, sdfsdfsdf
08-09 20:58:29.503 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, sdfsdfsdf
08-09 20:58:36.427 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, sdfsdfsdf
08-09 20:58:42.497 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, sdfsdfsdfs
08-09 20:58:48.164 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, test@test.com
08-09 20:59:04.594 💙 INFO logIn.internal + 619 - called log in Dsfsdfsdfsd, test@test.com
Вход в полноэкранный режим Выход из полноэкранного режима

⌛️ Закрытие

Вторая часть этого поста была немного скоростной, поэтому я рекомендую заглянуть в репозиторий. Я понял большую часть того, как это настроить, просмотрев примеры репо на GitHub, перечисленные на странице примеров decompose.

В следующей части серии я попытаюсь подключить аутентификацию Firebase к представлению входа, чтобы мы могли действительно получить некоторую интеграцию, а не базовое протоколирование.

Я, вероятно, сделаю небольшой перерыв в этом проекте, но в конце концов вернусь, так как я все еще хочу выполнить свое обещание не давать этому проекту заглохнуть, пока я не почувствую себя завершенным.

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