How to Develop Multiplatform apps using Compose Multiplatform for iOS, Android, Desktop and Web

How to do cross platform and multiplatform app development using Compose Multiplatform which is part of Kotlin Multiplatform Shared UI and Shared logic template for developing iOS and Android Apps | Part 1

Arunabh Das
Developers Inc
6 min readMar 10, 2024

--

Compose Multiplatform using KMP

Introduction

Compose Multiplatform, is a cross-platform technology that enables developers to write desktop, web, Android, and iOS applications using the Kotlin programming language. With the power of Compose Multiplatform, one can share code across different platforms, increasing productivity and ensuring consistency across various user experiences.

This codelab will guide you through setting up a Compose Multiplatform project with build targets for Android, iOS, and Desktop. Additionally, we will integrate Voyager, a powerful library for navigation in Kotlin Multiplatform projects, to manage navigation in a more structured and concise way.

Background

Kotlin Multiplatform comes in many flavors as seen in the below template gallery

KMP Template Gallery

We shall use the Shared UI Multiplatform App using Compose Multiplatform for the Lithium CRM app.

Prerequisites

Before start, please ensure you have the prerequisites

Start by setting up the environment for multiplatform development as outlined in the instructions here.

Step 1: Setting Up the Project

Setup the project according using the Kotlin Multiplatform Wizard at the following link

https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-create-first-app.html

Please select the following options

Kotlin Multiplatform Wizard

We may click on the Download button on the wizard and unzip the Lithium.zip file to view the scaffolding setup by the KMP wizard.

Opening the Lithium in Fleet IDE or AndroidStudio reveals the following folder structure

Compose Multiplatform Lithium CRM app folder structure
Compose Multiplatform Lithium CRM app folder structure

Step 2 — Add dependencies for Voyager

Modify the composeApp/build.gradle.kts file to add the Voyager dependencies as below

import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
}

kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}

listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}

sourceSets {
// Voyager
val voyagerVersion = "1.0.0-rc07"
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)

// Voyager
implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
}
}
}

android {
namespace = "app.lithium.lithiumapp"
compileSdk = libs.versions.android.compileSdk.get().toInt()

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
applicationId = "app.lithium.lithiumapp"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
}

Step 3 — Implement ScreenHome which implements Screen interface

We can now proceed to implement the ScreenHome which implements the Content composable as below

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen

class ScreenHome: Screen {
@Composable
override fun Content() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Home")
Button(
onClick = {

}
) {
Text("Navigate to Detail")
}
}
}
}

Step 4 — Implement ScreenDetail which implements Screen interface

Implement the Screen interface by implementing the Content Composable for ScreenDetail as below


class ScreenDetail: Screen {
@Composable
override fun Content() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Detail")
Button(
onClick = {

}
) {
Text("Navigate back to Home")
}
}
}
}

Step 5 — Call push on the navigator to navigate to Detail screen from Home screen

Modify ScreenHome.kt as below to implement the onClick lambda


class ScreenHome: Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Home")
Button(
onClick = {
navigator.push(ScreenDetail())
}
) {
Text("Navigate to Detail")
}
}
}
}

Step 6 — Modify App.kt to provide Navigator passing ScreenHome() as the parameter to the Navitator

Modify App.kt as below

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.Navigator
import org.jetbrains.compose.ui.tooling.preview.Preview

@Composable
@Preview
fun App() {
MaterialTheme {
Navigator(screen = ScreenHome())
}
}

Step 7 — Run the app to test that navigation from ScreenHome to ScreenDetail works

Running the app shows the following results

Test to see that navigation using Voyager Navigator is working

Step 8 — Add navigation transition animation

Add navigation transition by implementing SlideTransition in the Navigator lambda which takes navigator as a parameter


@Composable
@Preview
fun App() {
MaterialTheme {
Navigator(screen = ScreenHome()) {navigator ->
SlideTransition(navigator)
}
}
}

Step 9 — Add TopAppBar

Additionally, we may add a TopAppBar to App.kt as below -


@Composable
@Preview
fun App() {
MaterialTheme {
Navigator(screen = ScreenHome()) {navigator ->
Scaffold(
topBar = {
TopAppBar {
Text("Lithium CRM app")
}
}
) { innerPadding ->
SlideTransition(
navigator = navigator,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}

This yields the following results, with a smooth navigation between ScreenHome and ScreenDetail —

Lithium CRM app with Voyager navigation

Step 10— Passing Arguments between screens

In order to pass arguments between screens, we may first modify ScreenDetail to accept the textString argument as a value in the screen as below


data class ScreenDetail(
val textString: String
): Screen {
@Composable
override fun Content() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Detail")
Text(textString)
Button(
onClick = {

}
) {
Text("Navigate back to Home")
}
}
}
}

In addition, let us modify ScreenHome to pass the textString argument in to ScreenDetail as below


class ScreenHome: Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Home")
Button(
onClick = {
navigator.push(ScreenDetail(
textString = "Details About Lithium CRM"
))
}
) {
Text("Navigate to Detail")
}
}
}
}

Step 11 — Run to validate

Run to validate that the argument is being passed between screens

ScreenDetail with arguments

Source

The full source code for the above project can be found on Github at the repo below

Summary

In this part, we implemented navigation in a Compose Multiplatform project and passed arguments between screens using the Voyager Navigation library. In the next part, Part2, we shall see how to implement bottom navigation tab bar.

--

--

Arunabh Das
Developers Inc

Sort of an executive-officer-of-the-week of a-techno-syndicalist commune. Cypherpunk, techno-idealist, peacenik, spiritual, humanist