Getting Started with Kotlin Multiplatform[2/n]: Moko Shared View Model & Koin DI | by Debanshu Datta | Nov, 2023
After successfully implementing the basic Kotlin multiplatform app in our last blog, we will now focus on following a simple architecture with use cases, dependency injection, Shared View Model etc.
We will build on what we built in the first part of the blog. We make the code structured and better state management.
You can access the code, here on the branch feature/koin-vm
We will be using these dependencies:
We will start off by adding the required dependencies to the shared
module in build.gradle
[Preview]. Make sure to use the Koin 3.2.0
version, there was some compatibility issue with the latest version.
cocoapods {
.....
framework {
baseName = "MultiPlatformLibrary" // changed this from `shared`
export("dev.icerock.moko:mvvm-core:0.16.1")
export("dev.icerock.moko:mvvm-flow:0.16.1")
}
}//Common Dependencies with go under commonMain
val commonMain by getting {
dependencies {
....
api("dev.icerock.moko:mvvm-core:0.16.1")
api("dev.icerock.moko:mvvm-flow:0.16.1")
implementation("io.insert-koin:koin-core:3.2.0")
}
}
//Adding Android Specific dependency
val androidMain by getting {
dependencies {
implementation("io.insert-koin:koin-android:3.2.0")
}
}
Adding dependencies to androidApp
in build.gradle(:andriodApp)
[Preview]
dependencies {
.....
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")val koin = "3.5.0"
implementation("io.insert-koin:koin-android:${koin}")
implementation("io.insert-koin:koin-androidx-compose:${koin}")
}
Adding pod to iosApp
in Podfile(:iosApp)
[Preview]
target 'iosApp' do
...
pod 'mokoMvvmFlowSwiftUI', :podspec => 'https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.16.1/mokoMvvmFlowSwiftUI.podspec'
end
Firstly we can see the attached image, we have divided it into model
, repository
, usecase
, di
, utils
.
Model — consists of the data class which is serialized from the response in shared/src/commonMain/kotlin/com/debanshu/animax/data/model
Utils — is a simple example of using different dispatchers based on the platform.
/*
File Name: Dispatcher.kt
Loaction: shared/src/commonMain/kotlin/com/debanshu/animax/utils/Dispatcher.kt
*/
internal interface Dispatcher {
val io:CoroutineDispatcher
}internal expect fun provideDispatcher():Dispatcher
/*
File Name: Dispatcher.kt
Loaction: shared/src/iosMain/kotlin/com/debanshu/animax/utils/Dispatcher.kt
*/
internal class IosDispatcher:Dispatcher {
override val io: CoroutineDispatcher
get() = Dispatchers.Default
}
internal actual fun provideDispatcher():Dispatcher = IosDispatcher()
/*
File Name: Dispatcher.kt
Loaction: shared/src/androidMain/kotlin/com/debanshu/animax/utils/Dispatcher.kt
*/
internal class AndroidDispatcher:Dispatcher {
override val io: CoroutineDispatcher
get() = Dispatchers.IO
}
internal actual fun provideDispatcher():Dispatcher = AndroidDispatcher()
Repository — module consists of RemoteDataRepository
which makes the actual API call. networkClient
is coming from NetworkClient
which we have previously discussed in our previous blog.
/*
File Name: RemoteDataRepository.kt
Loaction: shared/src/commonMain/kotlin/com/debanshu/animax/data/repository/RemoteDataRepository.kt
*/
import com.debanshu.animax.data.model.TopAnimeResponse
import com.debanshu.animax.data.networkClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.urlinternal class RemoteDataRepository {
suspend fun getTopAnimeList(): TopAnimeResponse = networkClient.get {
url("https://api.jikan.moe/v4/top/anime")
}.body()
}
//TopAnimeResponse is the serialized data class from the model
Usecase — To make it extendable we will make an abstract BaseUsecase
and then we can implement it as GetTopAnimeUsecase
. We will also put model cases here TopAnimeResponse
is a model class inside shared/src/commonMain/kotlin/com.debanshu.animax/model
[code preview]. We are also using KoinComponent
and by inject()
, A class implementation KoinComponent
is similar to a Spring @Component
. It has a link to the global Koin
instance and serves as an entry point to the object tree encoded in the modules. by inject()
lazily inject instance from Koin. In this case, we will be injecting remoteRepository
& dispatcher
to our use case.
/*
File Name: BaseUseCase.kt
Loaction: shared/src/iosMain/kotlin/com.debanshu.animax/usecase
*/
abstract class BaseUseCase<REQUEST,RESPONSE> {
@Throws(Exception::class)
abstract suspend fun execute(request: REQUEST):RESPONSE
}
/*
File Name: GetTopAnimeUseCase.kt
Loaction: shared/src/iosMain/kotlin/com.debanshu.animax/usecase
*/
class GetTopAnimeUseCase : BaseUseCase<Unit, TopAnimeResponse>(), KoinComponent {
private val remoteDataRepository: RemoteDataRepository by inject()
private val dispatcher: Dispatcher by inject()
override suspend fun execute(request: Unit): TopAnimeResponse = withContext(dispatcher.io) {
remoteDataRepository.getTopAnimeList()
}
}
ViewModel — We will be using moko-viewModel
which provides us with ViewModel as we use it in Android. There is no specific difference between the MVVM approach we follow. We will be consuming this ViewModel in our view, while we will be emitting AnimeListState
from ViewModel. We will be injecting the GetTopAnimeUseCase
to ViewModel and consuming and triggering in our loadMovies()
/*
File Name: AppViewModel.kt
Loaction: shared/src/commonMain/kotlin/com/debanshu/animax/data/AppViewModel.kt
*/class AppViewModel(private val getTopAnimeUseCase: GetTopAnimeUseCase) : ViewModel() {
private val animeMutable =
MutableStateFlow<AnimeListState>(AnimeListState.Uninitialized)
val animeState = animeMutable.asStateFlow().cStateFlow()
init {
loadMovies()
}
private fun loadMovies() {
animeMutable.value = AnimeListState.Loading
viewModelScope.launch {
try {
animeMutable.value = AnimeListState.Success(getTopAnimeUseCase.execute(Unit).data)
} catch (e: Exception) {
e.printStackTrace()
animeMutable.value = AnimeListState.Error(e.message.orEmpty())
}
}
}
override fun onCleared() {
viewModelScope.cancel()
super.onCleared()
}
}
sealed interface AnimeListState {
data class Success(val data: List<Anime>) : AnimeListState
data class Error(val exceptionMessage: String) : AnimeListState
data object Loading : AnimeListState
data object Uninitialized : AnimeListState
}
There is no specific difference between the usage of Koin in KMP from our Native Android development. But to give iOS support the shared module KoinHelper.kt
needs to be added. We need to start Koin with our iOS application. In the Kotlin shared code, we have a function to let us configure Koin. We will be discussing getSharedModules()
this is a bit later.
/*
File Name: KoinHelper.kt
Loaction: shared/src/iosMain/kotlin/com/debanshu/animax/utils/KoinHelper.kt
*/import com.debanshu.animax.data.AppViewModel
import com.debanshu.animax.di.getSharedModules
import org.koin.core.component.KoinComponent
import org.koin.core.context.startKoin
import org.koin.core.component.get
import org.koin.dsl.module
fun initKoin() {
startKoin {
modules(getSharedModules())
}
}
class KoinHelper: KoinComponent {
fun getAppViewModel() = get<AppViewModel>()
}
In the iOS main entry, we can call the KoinHelperKt.doInitKoin()
function that is calling our helper function above.
/*
File Name: iOSApp.swift
Loaction: iosApp/iosApp/iOSApp.swift
*/
import SwiftUI
import MultiPlatformLibrary@main
struct iOSApp: App {
init(){
KoinHelperKt.doInitKoin()
}
var body: some Scene {
WindowGroup {
GridViewAnime()
}
}
}
In the Android main entry, We need to start Koin with our Android application. Just call the startKoin()
function in the application’s main entry point, our AnimaxApplication
class. Make sure to add AnimaxApplication
to your AndroidManifest.xml
.
/*
File Name: AnimaxApplication.kt
Loaction: androidApp/src/main/java/com/debanshu/animax/android/AnimaxApplication.kt
*/
import android.app.Application
import com.debanshu.animax.di.getSharedModules
import org.koin.core.context.startKoinclass AnimaxApplication:Application() {
override fun onCreate() {
super.onCreate()
startKoin {
modules(getSharedModules())
}
}
}
We will also set up getViewModelByPlatfrom()
which will be actual/expect function.
/*
File Name: viewModelModules.kt
Loaction: shared/src/commonMain/kotlin/com/debanshu/animax/di/viewModelModules.kt
*/
import org.koin.core.module.Moduleinternal expect fun getViewModelByPlatform(): Module
/*
File Name: viewModelModules.kt
Loaction: shared/src/androidMain/kotlin/com/debanshu/animax/di/viewModelModules.kt
*/
import com.debanshu.animax.data.AppViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
actual fun getViewModelByPlatform() = module {
viewModel {
AppViewModel(get())
}
}
/*
File Name: viewModelModules.kt
Loaction: shared/src/iosMain/kotlin/com/debanshu/animax/di/viewModelModules.kt
*/
import com.debanshu.animax.data.AppViewModel
import org.koin.dsl.module
actual fun getViewModelByPlatform() = module {
single {
AppViewModel(get())
}
}
Now, let’s set up the dependencies RemoteDataRepository
, GetTopAnimeUseCases
. To set up the dispatchers from the utils we discussed previously provideDispatcher()
. Finally, we will put getViewModelByPlatform()
the above to use the specific implementation of the platform.
/*
File Name: sharedModules.kt
Loaction: shared/src/commonMain/kotlin/com/debanshu/animax/di/sharedModules.kt
*/
import com.debanshu.animax.data.repository.RemoteDataRepository
import com.debanshu.animax.data.usecase.GetTopAnimeUseCase
import com.debanshu.animax.utils.provideDispatcher
import org.koin.dsl.moduleprivate val dataModule = module {
single { RemoteDataRepository() }
factory { GetTopAnimeUseCase() }
}
private val utilityModule = module {
factory { provideDispatcher() }
}
private val sharedModules = listOf(dataModule, utilityModule, getViewModelByPlatform())
fun getSharedModules() = sharedModules
Consuming the ViewModel data is something similar to what we follow in Android development.
Android Implementation
/*
File Name: MainActivity.kt
Loaction: androidApp/src/main/java/com/debanshu/animax/android/MainActivity.kt
*/
@Composable
fun GridViewAnime(viewModel: AppViewModel = getViewModel()) {
val animeState by viewModel.animeState.collectAsStateWithLifecycle()Scaffold(topBar = {
TopAppBar {
Row(modifier = Modifier.padding(10.dp)) {
Text(text = "Animax")
}
}
}) { defaultPadding ->
when (animeState) {
is AnimeListState.Loading -> {}
is AnimeListState.Success -> {
// This is the Success UI
}
is AnimeListState.Error-> {}
is AnimeListState.Uninitialized->{}
}
}
}
iOS Implementation
/*
File Name: GridViewAnime.swift
Loaction: iosApp/iosApp/GridViewAnime.swift
*/
import SwiftUI
import MultiPlatformLibrary
import mokoMvvmFlowSwiftUIstruct GridViewAnime: View {
@ObservedObject var viewModel: AppViewModel = KoinHelper().getAppViewModel()
@State var uiState: AnimeListState = AnimeListStateUninitialized()
private let adaptaiveColumns = [
GridItem(.adaptive(minimum: 170))
]
var body: some View {
let appUiState = viewModel.animeState
NavigationView{
VStack{
switch(uiState){
case is AnimeListStateLoading:
LoadingView()
case let successState as AnimeListStateSuccess:
// This is the Success UI
case is AnimeListStateError:
ErrorView()
default:
ErrorView()
}
}
.padding([.horizontal])
.navigationTitle("Animax")
}.onAppear {
appUiState.subscribe { state in
self.uiState = state!
}
}
}
}
Finally, we are done with a basic Architecture Example of Kotlin Multiplatform Mobile✨.
For any doubts and suggestions, you can reach out on my Instagram, or LinkedIn. Follow me for Kotlin content and more. Happy Coding!
I will well appreciate one of these 👏
Related Posts
Leave a Reply Cancel reply
Categories
- ! Без рубрики (1)
- ++PU (1)
- 1 (1)
- 1w (1)
- 1win Brazil (1)
- 1win India (1)
- 1WIN Official In Russia (1)
- 1win Turkiye (1)
- 1xbet egypt (1)
- 2ankarafayansustasi.net_may (1)
- ankarafayansustasi.netsiteai apr (1)
- Artificial intelligence (1)
- Arts & Entertainment, Photography (1)
- belugasitesi_mAY (1)
- BH_TOPsitesi apr (1)
- BHsitesy_may (2)
- Blog (3)
- Bookkeeping (14)
- Bootcamp de programação (2)
- Bootcamp de programación (2)
- BT_TOPsitesi apr (1)
- casino (5)
- casinom-hub (1)
- casinom-hub.comsitesi apr (3)
- colombian mail order brides (1)
- Cryptocurrency exchange (2)
- Dinamobet_next (1)
- Disease & Illness, Colon Cancer (1)
- Dumanbet (1)
- Dumanbet_next (1)
- Finance, Insurance (1)
- FinTech (5)
- Forex Trading (11)
- Galabet (1)
- Health & Fitness, Fitness Equipment (1)
- Hitbet (1)
- Home & Family, Crafts (1)
- Home & Family, Gardening (1)
- Internet Business, Audio-Video Streaming (1)
- Internet Business, Ecommerce (1)
- Internet Business, Email Marketing (1)
- Internet Business, Internet Marketing (1)
- IT Вакансії (1)
- IT Образование (5)
- IT Освіта (1)
- latin women dating (1)
- mail order bride (1)
- Mars bahis (2)
- Matadorbet (1)
- minimiri.comsitesi apr (3)
- Mobile App Development (771)
- Mostbet Russia (1)
- New Post (1)
- News (12)
- PB_TOPsitesi apr (1)
- PBsitesi_may (1)
- Pusulabet (1)
- redmirepool.bizsitesi apr (2)
- redmirepoolsitesi_may (1)
- Reference & Education, College (1)
- Reference & Education, Sociology (1)
- Rokusitesi apr (1)
- Sober living (6)
- Society, Divorce (1)
- Software development (7)
- Superbetin (1)
- Tempobet_next (1)
- thelongeststride.comsitesi apr (1)
- tipobet-turkiyesitesi apr (1)
- Ultrabet (1)
- Uncategorized (1)
- Игра (2)
- казино (1)
- Криптовалюты (1)
- Новости Криптовалют (1)
- Финтех (7)
- Форекс Брокеры (9)
- Форекс обучение (2)