DEV Community

Ryan Belz
Ryan Belz

Posted on

Learning Kotlin Multiplatfom Mobile: Entry 3.5

This is a continuation of the last post Learning KMM: Entry 3.

Instead of installment 4, I went ahead and used my big developer brain and incremented this version number to 3.5 because it does not truly feel like a version 4.


๐Ÿ“ Intro

In the last post we went over the Model and Intent part of MVI. In this post we are going to be going over the View part.

In the previous posts in this series I had already made two basic login screens using both Jetpack Compose and SwiftUI here.

This post is mainly going to be about using MVI Kotlin in tandem with Decompose to create components that I can wire up to my views.

๐ŸŽถ Decompose

So what exactly is Decompose and why do I need to use it? Well you don't exactly need to use Decompose to use the created stores in MVI Kotlin if you are using a declarative UI like Jetpack Compose and SwiftUI. However, Decompose makes it easier to create life-cycle aware components (which is super helpful on android) and comes with a multi-platform router for navigation.

Because of this you are able to now share even more code between apps (navigation logic) and you have a better separation of concerns between UI and non-UI code.

Feel free to read more on the overview of Decompose here.

๐ŸŒณ The Root Component

Each view is going to wire up to a component but all components need to descend from a parent component. The beginning component is going to be the component for the entire application which will be referred to as the "Root" component.

The root component is also where the shared router navigation is going to live. Inside the root component you can find configurations for children components as well as the current router state of the application.

Im going to gloss over a bit of the implementation details of the root component to keep the focus on the Login view. I will probably revisit this later when talking about navigation within the application.

๐Ÿค– Login Component

Once the scaffolding of the root component is set up we can continue to wire up our Login View to the Login Store.

Since we will define the initial component of the Root component as the Login Component, we will automatically be directed to this Login View once the application loads.

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In both of these logic blocks you can see we are switching the specific type of "Child" configuration to the LoginView or LoginScreen and passing the child component which is our "LoginComponent".

You can see this login component here

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))
}
Enter fullscreen mode Exit fullscreen mode

Essentially what this class is doing is forwarding the actions invoked by the view onto the Login Store from the previous post. The model of the Login Store is also being exposed in a way that can be consumed in both Jetpack Compose and SwiftUI.

๐ŸŽถ Compose Code

Consuming the store is very straight forward using Jetpack Compose and looks like the following.

@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
    )
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿฃ SwiftUI Code

Consuming the store in SwiftUI is a little more complex than android. It involves some creating of quite a lot of extension code to wrap the observable value, component router state, and exposing the library values from MVI Kotlin and Essenty that we accomplished using gradle in part one of this blog post. Once all of the helper extensions are set up you are left with something like this:

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)
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Testing with Napier

Since the action of logging in is not yet wired up yet I figured I would take this time to research a multi-platform logging solution that I could use to at least verify the login action was working as intended.

After some research I stumbled upon Napier and it was very easy to setup.

After following the documentation and setting up the initialization calls I added logging to the login button function.

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"
}
Enter fullscreen mode Exit fullscreen mode

After running the android app and fake logging in you can see the following in the logs:

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
Enter fullscreen mode Exit fullscreen mode

And after running the iOS app:

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
Enter fullscreen mode Exit fullscreen mode

โŒ›๏ธ Closing

The second part of this post was a little speedy so I suggest taking a further look into the repo. I figured out most of how to set this up by looking at the sample GitHub repos listed on the decompose example page.

In the next part of the series I am going to try to get Firebase authentication hooked up to the login view so we can actually get some integration going and not some basic logging.

I will probably be taking a little break from this project but will be returning eventually as I still want to keep my self-promise of not letting this die off until I feel accomplished.

Top comments (0)