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
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
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
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
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
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 —
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
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.