Blog Infos
Author
Published
Topics
Published

Photo by Mark Basarab on Unsplash

 

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.

App Demonstration

You can access the code, here on the branch feature/koin-vm

We will be using these dependencies:

Dependencies Setup

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
Shared Module Structure Setup

Firstly we can see the attached image, we have divided it into modelrepositoryusecasediutils.

Application Architectural Structure

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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Kotlin Multiplatform- From “Hello World” to the Real World

By now you’ve surely heard of Kotlin Multiplatform, and maybe tried it out in a demo. Maybe you’ve even integrated some shared code into a production app.
Watch Video

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform ...
Touchlab

Kotlin Multiplatform- From “Hello World” to the Real World

Russell Wolf
Kotlin Multiplatform
Touchlab

Jobs

/*
  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.url

internal 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 Koininstance 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
}
Dependency with Koin in KMP

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.startKoin

class 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.Module

internal 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 RemoteDataRepositoryGetTopAnimeUseCases. 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.module

private val dataModule = module {
    single { RemoteDataRepository() }
    factory { GetTopAnimeUseCase() }
}

private val utilityModule = module {
    factory { provideDispatcher() }
}

private val sharedModules = listOf(dataModule, utilityModule, getViewModelByPlatform())

fun getSharedModules() = sharedModules
Consuming ViewModel In UI

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 mokoMvvmFlowSwiftUI

struct 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 👏

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
I love Swift enums, even though I am a Kotlin developer. And iOS devs…
READ MORE
blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE
blog
Kotlin Multiplatform Mobile (or simply Multiplatform Mobile) is a new SDK from JetBrains that…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu