So like I mentioned in Learning KMM: Entry 1...
to force myself to not drop this project I am going to attempt to write my experiences while learning using Kotlin Multiplatform Mobile.
I have not forgotten about this, just have been a little lazy busy 😅.
🥱 Just a forewarning this is going to be a bit of a longer read than the first two entries and I will be breaking it into two parts.
⌛️ TLDR: check out the repository and read the docs on MVI Kotlin and decompose here.
📸 Overview
So to get started, this entry is going to be all about creating reusable components and classes that contain business logic for logging into my barebone views.
To do this I utilized the following Kotlin packages
Now MVI architecture is very new to me and I might explain this a bit like an idiot, but nevertheless I will try my best.
⚔️ MVI vs MVVM
MVI stands for Model-View-Intent and is a:
architectural pattern that utilizes unidirectional data flow
Meaning that the data only flows in one direction from the model to the view and vice versa. This flow helps maintain a single source of truth and usually contains a single immutable state of the entire view. It, quite like MVVM, usually allows for the model to know nothing about the view.
MVVM stands for Model-View-ViewModel and is:
structured to separate program logic and user interface controls
MVVM is something that I am more familiar with. This pattern usually does not entail having the entire immutable view state inside of a component but, just like stated before, the view model is completely unaware of the view.
I know from brief reading that some prefer MVVM to MVI as it is less components and classes and easier to get "up and running" to hook views up to business logic components or view models.
Also from brief research, it seem that there are two popular packages/libraries that are commonly used for both MVI and MVVM in Kotlin multi-platfom mobile projects.
- MVI Kotlin by arkivanov
- moko mvvm by icerock development
I am going with MVI Kotlin purely on the fact that I know nothing about it so it will be a bigger learning experience.
🏗 Setup
So there are a few essential packages to add to your shared gradle file definition to get started.
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")
}
}
There are some pretty specific things going on here that need to be pointed out that I ran into. First, you need to make sure the iOS dependencies are marked as "api" instead of "implementation" and to be totally honest with you I do not know why 😅. Second, it is important to make sure you export these three libraries to your shared iOS framework so that you can them in your Swift code. In the Android project you can get away with adding these dependencies straight to the gradle file. Once you have all this set up you should be ready to roll 🎲.
🎨 Model creation
The first part of MVI is the model, you can't have MVI without the model!
In this case the model is actually really simple for just a two input login screen.
data class Model(
val username: String,
val password: String
)
🏪 The Store
The store is where the business logic is going to go and will accept all "intents" and "messages" and output all "labels" and "state".
With the MVI Kotlin package you can choose either the Reaktive or Coroutine extensions and create a store pretty. I picked Coroutines out of personal preference and experience.
First for our store we need to define our State.
internal data class State(
val username: String = "",
val password: String = ""
)
The "Intents" of the store need to be defined. I like to think of "Intents" as actions that "Intend to change the state". In this case there is three.
- One to update the username
- One to update the password
- One update to attempt to login
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
}
Now when an "Intent" gets sent to the store it is executed by the "Executer" which you will see shortly. When the "Intent" is executed it outputs a "Message". This "Message" contains whatever has happened as a result of the "Intent" executing. The output, or "Message", is then sent to a "Reducer" which reduces the state using the "Message". Now this sounds very confusing but it makes a little bit of sense once you see the implementation.
First the "Store" in all its glory ⭐️
@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"
}
And now the "Messages" 📨
private sealed interface Msg {
class UpdateUsername(val username: String): Msg
class UpdatePassword(val password: String): Msg
class LoggedIn(authToken: String) : Msg
}
Final Breakdown:
Maybe an easier way to explain is just walking through someone attempting to log in.
- The user loads up the login screen with the default state of a new Login data class (Empty string username and password).
- The user types a username and every keystroke sends a "Intent" to the executor.
- Since there is no logic when changing the username we immediately dispatch a "Message" containing the most recent username string.
- The "Reducer" picks up the new username and COPIES (emphasis copies) the entire state object with the new username.
- The same steps (2-4) is done with the password.
- Once the user has both entered a username and password they might tap a button to log in.
- In the executor for "log in" you can see us simulate some type of business logic hitting an Auth mechanism to validate the username and password and return a "Auth token".
- Once the token is returned we dispatch a message containing the token which could then be either copied into the state or hopefully in the future saved locally.
Since we actually have not wired up our auth (going to use Firebase Auth in an upcoming post hopefully 🤞) I added some logging to just double check what was being sent to the log in function.
Hopefully this explanation makes a bit of sense to you 🙂.
And here is where I am going to break into part two (technically part 4) of the series to talk about how to wire up the Login Store to the View.
Stay tuned for part 4 and as always you can review all code here in the Voix GitHub repository.
Top comments (0)