Write Once, Run Everywhere: Building with Kotlin and Compose Multiplatform
Vladislav Kochetov
Posted on July 28, 2023
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!
Posted on July 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.