DEV Community

Cover image for Write Once, Run Everywhere: Building with Kotlin and Compose Multiplatform
Vladislav Kochetov
Vladislav Kochetov

Posted on • Updated on

Write Once, Run Everywhere: Building with Kotlin and Compose Multiplatform

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.

Creation of new multiplatform project

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")
}
Enter fullscreen mode Exit fullscreen mode

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)

         /.../
     }
}
Enter fullscreen mode Exit fullscreen mode

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")
      }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

iOS

Let's adapt our Compose code for iOS.

// shared/iosMain/../FactasticApp.kt

fun MainViewController(): UIViewController {
    val viewModel = AppViewModel()
    return ComposeUIViewController {
        FactasticApp(viewModel)
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Choosing target platform for launch

Well done!

Android (Dark theme) iOS (Light theme)
Android (Dark theme) iOS (Light theme)

Resources:

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)

Collapse
 
nhatquang profile image
Nhat Quang

This is definitely better than Flutter.
Thanks!

Collapse
 
vladleesi profile image
Vladislav Kochetov

I wholeheartedly agree with your point! 😄

Collapse
 
hinley profile image
Hinley Leo

Great job ! I am using Flutter to build a multiplatform app, but this shows me another solution.
Thanks!

Collapse
 
vladleesi profile image
Vladislav Kochetov

Thanks a lot for your feedback!
Keep exploring different solutions for your multiplatform app development.

Collapse
 
xoangon profile image
Keep Calm ♣

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

Collapse
 
vladleesi profile image
Vladislav Kochetov

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.