Blog Infos
Author
Published
Topics
,
Published
Topics
,

I am writing this article while the government of my country has banned the internet in Iran. People are protesting in the streets to oppose the compulsory hijab and the killing of Mahsa Amini by the police. Android community, Use the hashtag #MahsaAmini on social media to show your support for Mahsa and the Iranian people.

Kotlin Multiplatform Mobile

I thought Kotlin Multiplatform Mobile was complicated to use before I decided to try it, but I found it to be super easy to use, so why not use KMM that offers the following advantages: (Read more here)

  1. Using KMM, developers can use a single codebase for the business logic of iOS and Android applications and write platform-specific code only as necessary, such as when implementing a native UI or working with platform-specific APIs.
  2. Since KMM is easy and fast to modify, it requires less maintenance.
  3. A native programming language is used in KMM, which is based on Kotlin.
  4. With appropriate planning, KMM can reduce development time by 30–40% for iOS, since only the UI layer needs to be written.

So let’s do magic. 🤩

  • As a first step, we have to create a KMM project, for which we should add the Kotlin Multiplatform Mobile plugin. (Make sure to read here for more information)

 

 

Each Kotlin Multiplatform Mobile project includes three modules:

  1. shared is a Kotlin module that contains the logic common for both Android and iOS applications — the code you share between platforms. It uses Gradle as the build system that helps you automate your build process. The shared module builds into an Android library and an iOS framework.
  2. androidApp is a Kotlin module that builds into an Android application. It uses Gradle as the build system. The androidApp module depends on and uses the shared module as a regular Android library.
  3. iOSApp is an Xcode project that builds into an iOS application. It depends on and uses the shared module as an iOS framework. The shared module can be used as a regular framework or as a CocoaPods dependency, based on what you’ve chosen in the previous step in iOS framework distribution. In this tutorial, it’s a regular framework dependency.

 

https://kotlinlang.org/docs/multiplatform-mobile-create-first-app.html#examine-the-project-structure

 

The shared module consists of three source sets: androidMaincommonMain, and iosMain.

  • The second step is to use Koin for dependency injection and Ktor for remote service after we have created our project. Add their library links in the dependencies section in build.gradle.kts in the shared module.
val ktorVersion = "2.0.2"
val koinVersion = "3.2.0"
val serializationVersion = "1.3.2"
val commonMain by getting {
dependencies {
// Ktor
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
// koin
implementation("io.insert-koin:koin-core:$koinVersion")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
}
}
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}

And add a serialization plugin in the plugin section.

plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization") version "1.5.0"
}
view raw KMM_plugin.kts hosted with ❤ by GitHub
  • Third, let’s have some fun with coding.

Create new directories in your shared module. I want to use clean architecture when writing my code, so we need data and a domain directory.

In the domain, we set the entity, interface of the repository, and use case.

Entity:

import kotlinx.serialization.Serializable
@Serializable
data class Entity(
val id: Int = 0,
val name: String = "",
val image: String = "",
val description: String = ""
)
view raw KMM_Entity.kt hosted with ❤ by GitHub

Repository interface:

interface Repository {
suspend fun getOrigami() : List<Entity>
}

Use case:

class GetOrigamiUseCase(private val repository: Repository) {
suspend fun invoke() = repository.getOrigami()
}
view raw KMM_UseCase.kt hosted with ❤ by GitHub

In data, set remote service, data source, and implementation of the repository.

Remote Service:

import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
fun createJson() = Json { isLenient = true; ignoreUnknownKeys = true }
fun createHttpClient(
json: Json
) = HttpClient {
install(ContentNegotiation) {
json(json)
}
}

Data Source:

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import org.koin.core.component.KoinComponent
interface RemoteDatasource {
suspend fun fetchOrigami(): List<Entity>
}
class RemoteDatasourceImpl(
private val client: HttpClient
) : KoinComponent, RemoteDatasource {
override suspend fun fetchOrigami(): List<Entity> {
return try {
val response = client.get("https://kashkool.basalam.com/server_driven_ui/type6")
if (response.status.value == 200)
response.body()
else
listOf()
} catch (e: Exception) {
listOf()
}
}
}

Repository implementation:

class RepositoryImpl(
private val remoteDatasource: RemoteDatasource
) : KoinComponent , Repository {
override suspend fun getOrigami(): List<Entity> = remoteDatasource.fetchOrigami()
}

To implement DI, we need to have our modules defined, how? Just like this.

import org.koin.core.Koin
import org.koin.core.context.startKoin
import org.koin.core.parameter.parametersOf
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module
import kotlin.reflect.KClass
fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin {
appDeclaration()
modules(commonModule, platformModule())
}
fun initKoin() = initKoin {}
val commonModule = module {
single { createJson() }
single { createHttpClient(get()) }
single<RemoteDatasource> { RemoteDatasourceImpl(get()) }
single<Repository> { RepositoryImpl(get()) }
single { GetOrigamiUseCase(get()) }
}
fun <T> Koin.getDependency(clazz: KClass<*>): T {
return get(clazz, null) { parametersOf(clazz.simpleName) } as T
}
view raw KMM_Injector.kt hosted with ❤ by GitHub

 

  • Now we are done with the shared module, in the fourth step, we need to write specific code for the presentation in both android and iOS applications.
Android

We must add the compose, coroutines, Koin, navigation, and coil libraries.

val coroutinesVersion = "1.6.1"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
val koinVersion = "3.2.0"
implementation("io.insert-koin:koin-android:$koinVersion")
val composeVersion = "1.1.1"
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.compose.material:material:$composeVersion")
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
implementation("androidx.activity:activity-compose:1.4.0")
val navVersion = "2.4.0-alpha07"
implementation("androidx.navigation:navigation-compose:$navVersion")
val coilVersion = "1.3.1"
implementation("io.coil-kt:coil-compose:$coilVersion")

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

Now let’s create our app class:

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.component.KoinComponent
class App : Application() , KoinComponent {
override fun onCreate() {
super.onCreate()
initKoin {
androidContext(this@App)
modules(appModule)
}
}
}
view raw KMM_APP.kt hosted with ❤ by GitHub

Don’t forget to add this class to your manifest file, and as I always forget, make sure to add internet permission as well.

Koin has been initialized and has set the app module due to injecting the view model into the activity.

App module:

import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val appModule = module {
viewModel { MainViewModel(get()) }
}

It’s time for activity, but a simple one that handles our navigation system.

class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Navigation(viewModel)
}
}
}

Here is the code for Compose navigation. If you are already familiar with it, great, if not, please read more about it here.

@Composable
fun Navigation(viewModel: MainViewModel) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.HomeScreen.route) {
composable(route = Screen.HomeScreen.route) {
HomeScreen(
navController = navController,
viewModel = viewModel
)
}
composable(
route = Screen.DetailScreen.route + "/{name}/{image}/{description}",
arguments = listOf(
navArgument("name") {
type = NavType.StringType
defaultValue = "name"
nullable = false
},
navArgument("image") {
type = NavType.StringType
defaultValue = "image"
nullable = false
},
navArgument("description") {
type = NavType.StringType
defaultValue = "description"
nullable = false
}
)
) { entry ->
DetailScreen(
navController = navController,
name = entry.arguments?.getString("name"),
image = entry.arguments?.getString("image"),
description = entry.arguments?.getString("description")
)
}
}
}

Since MVI fits best to handle the state of views, I used

’s BaseViewModel to create my view model class. I have one intent that gets data, and four states to show the view.

Intent class:

sealed class Intent : UiIntent {
object FetchOrigamiData : Intent()
}
view raw KMM_Intent.kt hosted with ❤ by GitHub

State class:

sealed class State : UiState {
object Idle : State()
object Error : State()
object Loading : State()
data class Origami(val origami: List<Entity>) : State()
}
view raw KMM_State.kt hosted with ❤ by GitHub

View Model:

class MainViewModel(
private val getOrigamiUseCase: GetOrigamiUseCase
) : BaseViewModel<Intent,State>() {
override fun createInitialState(): State = State.Idle
override fun handleIntent(intent: Intent) {
when(intent){
Intent.FetchOrigamiData -> getOrigami()
}
}
private fun getOrigami() = viewModelScope.launch {
setState { State.Loading }
val res = getOrigamiUseCase.invoke()
if (res.isEmpty())
setState { State.Error }
else
setState { State.Origami(res) }
}
}

Last but not least, there is the Android Home view:

@Composable
fun HomeScreen(
navController: NavController,
viewModel: MainViewModel = viewModel()
) {
val uiState = viewModel
.uiState
.collectAsState()
.value
when (uiState) {
State.Error -> ErrorBody()
State.Idle -> viewModel.sendIntent(Intent.FetchOrigamiData)
State.Loading -> LoadingBody()
is State.Origami -> HomeScreenBody(
navController = navController,
data = uiState.origami
)
}
}

 

Congratulations, Our journey is almost 70 percent complete now that we are done with Android.

iOS

Time For ios, my favorite part:

Koin needs to be initialized in the app, a view model created, and the state handled in the view, just like Android.

Let’s begin with the application class:

import SwiftUI
@main
struct iOSApp: App {
init(){
startKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
view raw KMM_app.swift hosted with ❤ by GitHub

Ios Koin injector:

import SwiftUI
import shared
func startKoin() {
_koin = InjectorKt.doInitKoin().koin
}
private var _koin: Koin_coreKoin?
var koin: Koin_coreKoin {
return _koin!
}
extension Koin_coreKoin {
func get() -> GetOrigamiUseCase {
return koin.getDependency(objCClass: GetOrigamiUseCase.self) as! GetOrigamiUseCase
}
}

you must import a shared module to use it as I did. Also simply created an extension to use the use case in our swift project.

And don’t forget that in your platform class in the iosMain in the shared module, you need to add this method to use in Swift.

fun <T> Koin.getDependency(objCClass: ObjCClass): T? = getOriginalKotlinClass(objCClass)?.let {
getDependency(it)
}
view raw Swift_Koin.kt hosted with ❤ by GitHub

Then create an enum with three values to handle the state:

import Foundation
import shared
enum DataState {
case loading
case result([Entity])
case error
}

And create a view model to get data from our Kotlin use case class:

import Foundation
import shared
class ViewModel: ObservableObject {
@Published var dataState = DataState.loading
private let getOrigamiUseCase: GetOrigamiUseCase = koin.get()
func loadOrigamies() {
self.dataState = DataState.loading
getOrigamiUseCase.invoke {entity, error in
if let entity = entity {
self.dataState = DataState.result(entity)
} else {
self.dataState = DataState.error
}
}
}
}

Using your Kotlin classes in a Swift project is as simple as using koin.get() if you’ve defined them in your Injector Swift class.

Finally, use the view model in the home view and show the view in its state as follows:

import SwiftUI
import shared
struct ContentView: View {
let greet = Greeting().greeting()
@ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack(){
NavigationView{
uiState()
.navigationTitle("KMM Origami")
}
}
.onAppear(){
viewModel.loadOrigamies()
}
}
private func uiState() -> AnyView {
switch viewModel.dataState {
case DataState.loading:
return AnyView( ZStack{
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .gray))
.scaleEffect(2)
}.multilineTextAlignment(.center))
case DataState.result(let origimies):
return AnyView(
List(origimies) { origimi in
NavigationLink {
DetailView(entity: origimi)
.navigationTitle(origimi.name)
} label: {
HomeView(entity: origimi)
}
}
)
case DataState.error:
return AnyView(Text("ERROR").multilineTextAlignment(.center))
}
}
}
extension Entity: Identifiable { }
view raw KMM_view.swift hosted with ❤ by GitHub

 

Our simple Android and iOS applications have been created, but is KMM ready for production? Is KMM reliable for large-scale projects? No, I don’t believe so.

How about you? Are you more likely to use KMM because of its benefits or two separate native applications?

Source code
#MahsaAmini — Iranian people

WE NEED YOUR HELP! Iran’s security forces continue to shoot and kill protesters — including women and children — while shutting down internet access so they can’t organize or communicate with the world.
Please demand that your officialscompanies, and friends show support for the Iranian people and condemn their oppressors. Android community, Use the hashtag #MahsaAmini on social media to show your support for Mahsa and the Iranian people.

This article was originally published on proandroiddev.com on September 30, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
First of all, let’s explain why I need the CookiesStorage. To identify the user,…
READ MORE
blog
Hey everyone, I want to share more of Kotlin Multiplatform Mobile Content since it…
READ MORE
blog
In my previous story, I’ve talked about why I believe we can strongly improve…
READ MORE
blog
As part of my daily routine, I often explore the latest developments on platforms…
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