Compose Multiplatform in Action: Implementing Cross-Platform UI with Compose in CMP
Introduction
Compose Multiplatform (CMP)
Yesterday we set up our common Material3 Theme
Today we can start creating the UI for our cross-platform app
In CMP
, we use Compose
to build both Android and iOS interfaces
and all our Compose UI is entirely within commonMain
In other words, the UI part can be fully shared
Since Android is now fully promoting the use of Compose
for native app development
those who have already mastered Compose are at an advantage
Creating Our First Compose Screen
- Let's first look at how to create a basic Hello World screen using Compose in
CMP
(The timeless example Hello world
XD)
Since Compose
uses a declarative UI
approach
you just need to add @Composable
before the function you want to implement
to turn it into a Compose UI component
Add the following to commonMain
in your CMP project
// in ~/commonMain/
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name !")
}
-
When you want to preview it
you just create another function and add
@Preview
before itthis allows you to see the Compose preview in your IDE
// in ~/commonMain/
@Preview
@Composable
fun GreetingPreview() { Greeting("Compose") }
You can actually see the @Preview
screen on the right side of the IDE
Modifier is a tool in
Compose
used to modify and configure components
It provides various functions to change the behavior and appearance of Compose UI components
If you type in a Modifier
and then open it to see
you'll find
it provides various options for setting UI behavior and appearance
such as backgroundcolor, align, height, width, onClick, etc.
there are many options - feel free to explore them:
-
If you followed along yesterday to create the Theme
you can try using the
Material3 theme
to set component background colors
// in ~/commonMain/
@OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
//Set Material 3 theme using ElegantAccessComposeTheme
ElegantAccessComposeTheme {
Greeting("Compose")
}
}
Then add a Column
outside the Text like this
and use Modifier.background(color = MaterialTheme.colorScheme.background)
// in ~/commonMain/
@Composable
fun Greeting(name: String) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background)
) {
Text(text = "Hello $name !")
}
}
-
When developing apps for Android or iOS
you often need to
customize the toolbar
-
We can create a reusable topbar like this
//in ~/commonMain/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainAppBar(
modifier: Modifier = Modifier,
config: MainAppBarConfig,
elevation: Dp = 4.dp,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer
) {
CenterAlignedTopAppBar(
title = config.title,
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = containerColor
),
modifier = modifier.shadow(elevation = elevation),
navigationIcon = {
config.navigationIcon()
},
actions = {
config.actionIcon?.invoke()
}
)
}
The core concepts
here are:
-
Using Compose's native TopAppBar:
CenterAlignedTopAppBar
-
Considering that different screens may have different topbar content
we created a separate data class
MainAppBarConfig
when using the topBar
you don't need to rewrite the TopAppBar
just create an instance of
MainAppBarConfig
-
Frequently used variables are exposed
so they can be configured
for example:
elevation
Implementing the data class
MainAppBarConfig
You can customize commonly adjusted items
like title length, title text, style, back button icon, etc.
// in ~/commonMain/
data class MainAppBarConfig(
val marqueeNum: Int = 0,
val titleText: @Composable () -> String = { "" },
val title: @Composable () -> Unit = {
DefaultTitleText(titleText(), marqueeNum)
},
val navigationIcon: @Composable () -> Unit = {},
val actionIcon: @Composable (() -> Unit)? = null,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DefaultTitleText(titleText: String, marqueeNum: Int) {
Text(
modifier = Modifier.basicMarquee(marqueeNum),
text = titleText,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = ExtendedTheme.colors.onAppBar
)
}
It uses the MainAppBarConfig
we created earlier
and inputs the content you want to configure
// in ~/commonMain/
private fun createSettingConfig(
navController: NavController,
) = MainAppBarConfig(
titleText = { stringResource(Res.string.title_setting) },
navigationIcon = {
NavBackIcon(navController = navController)
},
)
Note: If you want to implement back button navigation functionality
you might need to pass in the navigation event to the function
However, using NavController
is more flexible
NavController
can manage all routes
you just need to specify the defined string when you need to navigate
Detailed explanations about this part will be provided in later chapters
- Practical usage
// in ~/commonMain/
@Composable
fun SettingScreen(navController: NavController) {
val config = createSettingConfig(navController)
Scaffold(
topBar = {
MainAppBar(config = config)
},
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {...}
}
Real Example
-
Using the concepts above
we can easily implement a Settings page
// in ~/commonMain/
@Composable
fun SettingScreen(navController: NavController) {
val config = createSettingConfig(navController)
Scaffold(
topBar = {
MainAppBar(config = config)
},
containerColor = MaterialTheme.colorScheme.surfaceVariant
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
Text(
"Choose Transfer Option",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 30.dp, top = 16.dp, end = 30.dp)
)
}
items(SettingOption.values()) { option ->
SettingOptionCard(
option = option,
onClick = {
navController.navigate(option.route) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
}