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)
- 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.
- Since KMM is easy and fast to modify, it requires less maintenance.
- A native programming language is used in KMM, which is based on Kotlin.
- 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:
- 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.
- 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.
- 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: androidMain
, commonMain
, 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" | |
} |
- 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 = "" | |
) |
Repository interface:
interface Repository { | |
suspend fun getOrigami() : List<Entity> | |
} |
Use case:
class GetOrigamiUseCase(private val repository: Repository) { | |
suspend fun invoke() = repository.getOrigami() | |
} |
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 | |
} |
- 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
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) | |
} | |
} | |
} |
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.
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() | |
} |
State class:
sealed class State : UiState { | |
object Idle : State() | |
object Error : State() | |
object Loading : State() | |
data class Origami(val origami: List<Entity>) : State() | |
} |
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() | |
} | |
} | |
} |
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) | |
} |
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 { } |
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 officials, companies, 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