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 it
    this 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
Cover

Modifiers in Compose Components

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:
Cover

  • 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 !")
    }
}
Creating a Compose Top App Bar
  • When developing apps for Android or iOS
    you often need to customize the toolbar
    Cover

  • 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:

  1. Using Compose’s native TopAppBar: CenterAlignedTopAppBar
  2. 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
  3. 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
    )
}
Practical Use of TopBar
  • Here we’ll create the createSetting function.
    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 ```kotlin // 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
                        }
                    }
                )
            }
        }
    }
}

You might also enjoy