MVI implementation in Kotlin Multiplatform using the Reaktive library (Part 2)

Randy Arba
8 min readSep 30, 2022

--

This Part one contain the basic of MVI pros and cons and also why i choose MVI for KMM.

Part Two will explore more about MVI implementation on KMM, also few library that support on it such as Ktor, Kotlin-Serialization, Jetpack Compose and Swift UI.

Preparation

Component that you need to prepare and take a note based on Experience:

  • Download KDoctor KDoctor works on macOS only. make sure when run Kdoctor all required component should available.
  • Install Commandline for Xcode (Xcode-cli), ensure command line version match with Xcode.
  • Note for Kdoctor, if result came with suggestion to relate Ruby version , so please download Ruby version that Kdoctor suggest using RVM. But if you have multiple ruby version you can choose one rvm use ruby-{version} on terminal.
  • After cli xcode installed so it must restart first before. sudo xcode-select -r
  • Check CocoaPods install and clang-llvm-apple component if needed.
  • if build ios still failing please remove framework file first or check cocoapod with pod setup, pod install, pod init, and cocoapods-generate install.
  • Then Run ./gradlew :shared:embedAndSignAppleFrameworkForXcode
  • Dont forget to install jdk full 18 to prevent failed when run/build in Xcode because it will run gradle script.
  • delete ios_app in framework directory, if failed find id Pods_iosApp.

Make sure you already have KMM plugin in Android Studio, if not please download from marketplace. Create new KMM Module (Kotlin Multi Platform Library) please specify iOS framework distribution as Regular Framework, wait until Android Studio success download all component, after success u should see project directory like this

AndroidApp contain Android module only all project relate Android will process on that directory, then iosApp module is project for Ios code like swift, etc the project only can be loaded using xcode.

Run for first time using android and Ios. make sure gradlew build is success then icon of run on Android studio not show Error (x).

MVI Pattern Preparation

Create another new library module (mvi) on Android studio, this module contain MVI pattern abstraction. then we need create parent module that have MVI module and Shared module. Parent module will rename as shared and for previously shared module will change into main. So it will become like this.

Shared directory will contain all shared module include MVI module and main share module. why we need separate becuase MVI module only contain MVI pattern abstraction that can be adjust and scoped on abstraction level.

Shared (Main Module) Intro

  • Please check gradle file from this link.

Main Module will accomodate shared business logic part that include commonMain, androidMain, and iOSMain, etc. It this module will depend on the kotlin gradle configuration and sourceSets, such as android(), jvm(), and ios binaries. So dont forget to make sure the name of your sourceSet is avalaible in this module.

commonMain directory will provide business logic specific whereas androidMain and iosMain is for platform specific and each of this module/domain can configure separate dependencies in gradle. Why we need separate androidMain and iosMain separation? because commonMain will free from platform dependencies so if you want to check each platform specific you can communicate androidApp and iOSApp module via actual and expect keywrod that reserved by kotlin KMM API. Below example need.

Okay lets have an example, there is usecase where in commonMain will handle function based on prod and debug behavior, so how commonMain validate each platform is debug version? we cant obtain platform specific on commonMain because it must be free all platform related.

so we must create this in commonMain module,

in commonMain module directory

After that we can create this code on iosMain and AndroidMain

in iosMain module directory
in androidMain module directory

Please take a note the stucture directory of file must same, example if those code (contain expect keyword) file in commonMain in root of directory so do in androidMain and iosMain, if structure of directory is different it will show error “Actual class ‘Config’ has no corresponding expected declaration” on iosMain and androidMain module that contain actual keyword. Below is sample structure.

  • commonMain -> platformDir -> file that contain expect keyword
  • iosMain -> platformDir -> file that contain actual keyword for iOS
  • androidMain -> platformDir -> file that contain actual keyword for android

Shared (MVI module)

  • Please check gradle file from this link.

TypeAliases Kotlin

Getting Started of MVI based on first blog that MVI have two basic (Store/model and View), but create the less depend class first using typealias to simplify return type of abstraction. For Intent processing, we use Actor a function that take current state and an Intent and return stream Effect

Typealias Actor that have State, Intent, and Effect processing type

After that we can introduce Reducer, a function that accept current State and an Effect result from actor and then return new state. We can include all the typealias in one file (Typealiases.kt)

TypeAlias Reduce that have State and Effect processing type
  1. Store/Model

Store is represent model in MVI, it accept Intent and return stream of State type, the current state will be emitted when subscribing to stream state. has two generic param input and output state, then extend Consumer of Intent, Observable State, Disposable type. After abstraction store already created and continue to create helper based from Store abstraction we call it StoreHelper. Why we need this because it’s not very convenient to implement such an interface every time.

Store interface for abstraction
StoreHelper

So StoreHelper will take three parameter, initialState, actor, and reducer. action and reducer use typealiase that we already define earlier. StoreHelper also have three generice parameter input Intent and Effect, then output State. Those class will extends State as Stream and have DisposableScope type (interface from Reaktive for managing subscriptions). Please take a look for first blog Store diagram.

There two function that created on StoreHelper, onIntent and onEffect.

OnIntent Method

  • Pass Intent argument
  • Invoke Actor then put current State and Intent, it will subscribes to the stream of Effects.
  • Pass stream onNext (Effects type) to the onEffect method
  • Stream of the subscription on the ThreadLocal (isThreadLocal) for avoids freezing in Kotlin/Native

OnEffect Method

  • Pass Effect argument
  • Put new State to the BehaviorSubject that we already define, which leads to emission of the new state for all subscribers
  • Invoke Reducer and passes the current State and the Effect to it

2. View

This is the main part of MVI which accept store and pass it into View via render, i can say MVI pattern different with other pattern is in this view. MVI only receive in one stream called render and then create view based on state. So i can say that view will created and compose via render (it match and fully support with Jetpack Compose and swiftUI).

First we need create abstraction MviView interface. it consist two generic parameter input model and output Event, accept model via render method and then return stream of event via event variable.

For client android/ios not direct interact with event and render property so we will create abstract class extending MviView interface.

MviView Interface
AbstractMviView class

Store produces States that are converted to Models and displayed by the View. then produces Events that converted to Intents and delivered to the Store for processing. This will eliminates coupling between Store and View. The View can work directly with States and Intents.

Shared (Main Module)

For example case, we use Pokemon Prefix name but the API using Anime Api hahaha still on development process in the future will be change.

Store
Create store interface that extend Store from Mvi module. this store will load all data related. This interface consist sealed class as Intent and State Data class. this interface define as internal, mean t will be visible everywhere in the same module.

So all action or intention to do will on this sealed class, because we only have one action/intention todo such as reload(). in other part like State data class we have flag isLoading and data. Data it self contain success data and error.

Okay after that we implement Store into StoreImp that take one parameter usecase and have disposableScope. below StoreImp class there is sealed class Effect is related of Loading states.

View

Create View that extend MviView from Mvi module the generic parameter will create on this interface, There two part Model data class and Event sealed class. This View class that consist model and event is part that communicate with the platform view. Event will fire from view platform and then model is data that pass on render to compose view.

Mapper

Mapper help to transform type into another type of object, in this part we transform State into Model and Event into Intent, maybe you ask why Event need to transform why not directly become Intent, because we try to decouple Action from View and from Store, so if we there is case if action/event from view can reuse same action/Intent on Store. Example;

We have two event in view that trigger Refresh and Search, but in Intent Store we can reuse Reload() that have same result.

Data Source

Data source is where Data come from and provide data into other part. Data source will consist Model, Repository, UseCase. Model that we mention here is not model in MVI pattern but model data from response API it contain Data class and serialization annotation example, SearchApiResponse and ResultResponse.

Repository

IndexRepository interface for abstraction
Implementation from IndexRepository

UseCase

UseCase Interface Abstraction
Create Index that implement from UseCase

Client API

--

--