Welcome to the world of Kotlin multiplatform app development! In this article, we'll explore a simple example of an app built entirely with Kotlin. We'll leverage the power of Kotlin multiplatform, Compose multiplatform, Kotlin Coroutines, Kotlin Serialization, and Ktor to create an app that runs smoothly on both Android and iOS platforms.
The focus here is on creating a multiplatform app that uses shared network logic and user interface only just on Kotlin.
We require macOS, Android Studio, Kotlin Multiplatform Mobile plugin, Xcode, and a dash of enthusiasm!
Disclaimer: I aim to focus solely on essential aspects. For the complete code, please refer to the following GitHub repository: https://github.com/vladleesi/factastic
So, let's get started!
To begin, let's create a multiplatform project in Android Studio by selecting "New Project" and then choosing the "Kotlin Multiplatform App" template.
After creating the multiplatform project in Android Studio, the next step is to add all the necessary dependencies and plugins.
// gradle.properties
org.jetbrains.compose.experimental.uikit.enabled=true
kotlin.native.cacheKind=none
// build.gradle.kts (app)
plugins {
kotlin("multiplatform").apply(false)
id("com.android.application").apply(false)
id("com.android.library").apply(false)
id("org.jetbrains.compose").apply(false)
kotlin("plugin.serialization").apply(false)
}
// settings.gradle.kts
plugins {
val kotlinVersion =extra["kotlin.version"] as String
val gradleVersion =extra["gradle.version"] as String
val composeVersion =extra["compose.version"] as String
kotlin("jvm").version(kotlinVersion)
kotlin("multiplatform").version(kotlinVersion)
kotlin("plugin.serialization").version(kotlinVersion)
kotlin("android").version(kotlinVersion)
id("com.android.application").version(gradleVersion)
id("com.android.library").version(gradleVersion)
id("org.jetbrains.compose").version(composeVersion)
}
repositories{
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
After setting up versions, we proceed to work on the platform-specific 'shared' module.
// build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
id("org.jetbrains.compose")
kotlin("plugin.serialization")
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
// IMPORTANTE: Include a static library instead of a dynamic one into the framework.
isStatic = true
}
}
val commonMain by getting {
dependencies {
// Compose Multiplatform
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
/.../
}
}
And add dependencies for Android & iOS http client specifics.
val androidMain by getting {
dependencies {
val ktorVersion = extra["ktor.version"] as String
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
}
}
val iosMain by getting {
dependencies {
val ktorVersion = extra["ktor.version"] as String
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
Now, it's time to dive into the UI development. We'll focus on creating a simple UI featuring a button to generate random useless facts from a server.
// shared/../FactasticApp.kt
@Composable
fun FactasticApp(viewModel: AppViewModel, modifier: Modifier = Modifier) {
FactasticTheme {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val state = viewModel.stateFlow.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadUselessFact()
}
MainScreen(state.value, viewModel::onClick)
}
}
}
Business logic inside
AppViewModel
is here.
Configuration of Ktor client is here.
Behold, the moment for the grand trick has arrived.
Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = AppViewModel()
setContent {
FactasticApp(viewModel)
}
}
}
iOS
Let's adapt our Compose code for iOS.
// shared/iosMain/../FactasticApp.kt
fun MainViewController(): UIViewController {
val viewModel = AppViewModel()
return ComposeUIViewController {
FactasticApp(viewModel)
}
}
Next, we should open Xcode to work on the iOS part, as we need to perform some Swift-related tasks. Locate iosApp/iosApp.xcodeproj
, right-click on it, and then choose "Open in" -> "Xcode."
In Xcode, create a new Swift file named "ComposeView.swift" by clicking on "File" -> "New" -> "File..." -> "Swift File" -> "Next" -> "ComposeView.swift" -> "Create."
Oh my God, what is this? Is it Swift?
// ComposeView.swift
import Foundation
import SwiftUI
import shared
struct ComposeView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// ignore
}
func makeUIViewController(context: Context) -> some UIViewController {
FactasticAppKt.MainViewController()
}
}
Make a minor update to the existing ContentView.swift
file in Xcode.
import SwiftUI
import shared
struct ContentView: View {
var body: some View {
// This one
ComposeView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
That's all the Xcode part, you may forget about it (if you can) and return to Android Studio.
Before running the app, we must build the iOS module using the following command: ./gradlew :shared:compileKotlinIosArm64
.
In Android Studio, select the target platform, and then click on the "Run" button to launch the application on the chosen platform.
Well done!
Android (Dark theme) | iOS (Light theme) |
---|---|
Resources:
- Kotlin Multiplatform for mobile
- Compose Multiplatform
- Kotlin Coroutines
- Kotlin Serialization
- Ktor
- Napier
Update as of 08/02/2023:
Additionally, I have incorporated the desktop module into the project.
P.S.
I'd greatly appreciate receiving feedback. If my examples happen to be useful to you, please, consider giving a star to the GitHub repository. Thank you!
Top comments (6)
This is definitely better than Flutter.
Thanks!
I wholeheartedly agree with your point! 😄
Great job ! I am using Flutter to build a multiplatform app, but this shows me another solution.
Thanks!
Thanks a lot for your feedback!
Keep exploring different solutions for your multiplatform app development.
Great and straightforward intro. I feel like there's much more insights on working with KMP that are missing here, but that may be out of the scope of what this post aims at being.
I wanted to point out a nuance to correct in this post: Hadi Hariri said in the DroidCon Germany 2023 that KMM turned out to be misleading and that it will no longer be used in favor of KMP
Thanks for it! You are right, KMM is a versatile term distinguishing diverse KMP use cases to emphasize that we only cover the part that focuses on mobile development (Android and iOS).
For example, the plugin we use in Android Studio is still called KMM plugins.jetbrains.com/plugin/14936....
But I really should use KMP instead of KMM, the terms in the article will be updated.