Kotlin Multiplatform Help

Create your own application

Now that you've explored and enhanced the sample project created by the wizard, you can create your own application from scratch, using concepts you already know and introducing some new ones.

You'll create a "Local time application" where users can enter their country and city, and the app will display the time in the capital city of that country. All the functionality of your Compose Multiplatform app will be implemented in common code using multiplatform libraries. It'll load and display images within a dropdown menu and will use events, styles, themes, modifiers, and layouts.

At each stage, you can run the application on all three platforms (iOS, Android, and desktop), or you can focus on the specific platforms that best suit your needs.

Lay the foundation

To get started, implement a new App() composable:

  1. In shared/src/commonMain/kotlin, open the App.kt file and replace the code with the following App() composable:

    @Composable @Preview fun App() { MaterialTheme { var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), ) { Text(timeAtLocation) Button(onClick = { timeAtLocation = "13:30" }) { Text("Show Time At Location") } } } }
    • The layout is a column containing two composables. The first is a Text composable, and the second is a Button.

    • The two composables are linked by a single shared state, namely the timeAtLocation property. The Text composable is an observer of this state.

    • The Button composable changes the state using the onClick event handler.

  2. Run the application on Android and iOS:

    New Compose Multiplatform app on Android and iOS

    When you run your application and click the button, the hardcoded time, 13:30, is displayed.

  3. Run the application on the desktop using Compose Hot Reload by starting the desktopApp [hot] 🔥 run configuration. The app works, but the window looks mismatched with the UI:

    New Compose Multiplatform app on desktop
  4. To fix this, update the main.kt file in the desktopApp/src/kotlin directory as follows:

    fun main() = application { val state = rememberWindowState( size = DpSize(400.dp, 350.dp), position = WindowPosition(300.dp, 300.dp) ) Window( title = "Local Time App", onCloseRequest = ::exitApplication, state = state, alwaysOnTop = true ) { App() } }

    Here, you set the title of the window and use the WindowState type to give the window an initial size and position on the screen.

  5. Follow the IDE's instructions to import the missing dependencies.

  6. To see the app automatically update, save any modified files (⌘ S/Ctrl+S). Its appearance should improve:

    Smaller window of the Compose Multiplatform app on desktop
    Compose Hot Reload

Support user input

Now let users enter the name of a city to see the time at that location. The simplest way to achieve this is by adding a TextField composable:

  1. Replace the current implementation of App() in commonMain/kotlin/compose.project.demo/App.kt with the one below:

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), ) { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = "13:30" }) { Text("Show Time At Location") } } } }

    The new code adds both the TextField and a location property. As the user types into the text field, the value of the property is incrementally updated using the onValueChange event handler.

  2. Follow the IDE's suggestions to import the missing dependencies.

  3. Run the application on each platform you're targeting. The time displayed is still hardcoded, but now you can enter a timezone in the text field:

User input in the Compose Multiplatform app on Android and iOS
User input in the Compose Multiplatform app on desktop
User input in the Compose Multiplatform app on the web

Calculate time

The next step is to use the given input to calculate time. To do this, create a currentTimeAt() function:

  1. Return to the shared/src/commonMain/kotlin/compose.project.demo/App.kt file and add the following function:

    fun currentTimeAt(location: String): String? { fun LocalTime.formatted() = "$hour:$minute:$second" return try { val time = Clock.System.now() val zone = TimeZone.of(location) val localTime = time.toLocalDateTime(zone).time "The time in $location is ${localTime.formatted()}" } catch (ex: IllegalTimeZoneException) { null } }

    This function is similar to todaysDate(), which you created earlier and which is no longer required.

  2. Follow the IDE's instructions to import the missing dependencies. Make sure to import the Clock class from kotlin.time, not kotlinx.datetime.

  3. Adjust your App composable to invoke currentTimeAt():

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize() ) { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" }) { Text("Show Time At Location") } } } }
  4. Run the application again and enter a valid timezone.

  5. Click the button. You should see the correct time:

Time display in the Compose Multiplatform app on Android and iOS
Time display in the Compose Multiplatform app on desktop
Time display in the Compose Multiplatform app on the web

Improve the style

The application is working, but there are issues with its appearance. The composables could be spaced better, and the time message could be rendered more prominently.

  1. To address these issues, use the following version of the App composable:

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) TextField( value = location, onValueChange = { location = it }, modifier = Modifier.padding(top = 10.dp) ) Button( onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" }, modifier = Modifier.padding(top = 10.dp) ) { Text("Show Time") } } } }
    • The modifier parameter adds padding all around the Column, as well as at the top of the Button and the TextField.

    • The Text composable fills the available horizontal space and centers its content.

    • The style parameter customizes the appearance of the Text.

  2. Follow the IDE's instructions to import the missing dependencies.

  3. Run the application to see how the appearance has improved:

Improved style of the Compose Multiplatform app on Android and iOS
Improved style of the Compose Multiplatform app on desktop
Improved style of the Compose Multiplatform app on the web

Refactor the UI

The application works, but it's susceptible to typos. For example, if a user enters "Franse" instead of "France", the app won't be able to process that input. It would be easier for users to pick countries from a predefined list.

  1. To achieve this, update the App() composable and the currentTimeAt() function, adding an auxiliary data class:

    data class Country(val name: String, val zone: TimeZone) fun currentTimeAt(location: String, zone: TimeZone): String { fun LocalTime.formatted() = "$hour:$minute:$second" val time = Clock.System.now() val localTime = time.toLocalDateTime(zone).time return "The time in $location is ${localTime.formatted()}" } fun countries() = listOf( Country("Japan", TimeZone.of("Asia/Tokyo")), Country("France", TimeZone.of("Europe/Paris")), Country("Mexico", TimeZone.of("America/Mexico_City")), Country("Indonesia", TimeZone.of("Asia/Jakarta")), Country("Egypt", TimeZone.of("Africa/Cairo")), ) @Composable @Preview fun App(countries: List<Country> = countries()) { MaterialTheme { var showCountries by remember { mutableStateOf(false) } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) Row(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { DropdownMenu( expanded = showCountries, onDismissRequest = { showCountries = false } ) { countries().forEach { (name, zone) -> DropdownMenuItem( text = { Text(name)}, onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) } } } Button(modifier = Modifier.padding(start = 20.dp, top = 10.dp), onClick = { showCountries = !showCountries }) { Text("Select Location") } } } }
    • There is a Country type, consisting of a name and a timezone.

    • The currentTimeAt() function takes a TimeZone as its second parameter.

    • The App now requires a list of countries as a parameter. The countries() function provides the list.

    • DropdownMenu has replaced the TextField. The value of the showCountries property determines the visibility of the DropdownMenu. There is a DropdownMenuItem for each country.

  2. Follow the IDE's instructions to import the missing dependencies. When importing Row(), pick the @Composable version.

  3. Run the application to see the redesigned version:

The country list in the Compose Multiplatform app on Android and iOS
The country list in the Compose Multiplatform app on desktop
The country list in the Compose Multiplatform app on the web

Introduce images

The list of country names works, but it's not a great user experience. You can improve the list by adding images of national flags next to country names.

Compose Multiplatform provides a library for accessing resources through common code across all platforms. The Kotlin Multiplatform wizard has already added and configured this library, so you can start loading resources right away.

To support images in your project, you'll need to download image files, store them in the correct directory, and add code to load and display them:

  1. Download flag images from Flag CDN to match the list of countries you have already created. In this case, these are Japan, France, Mexico, Indonesia, and Egypt.

  2. Move the images to the composeApp/src/commonMain/composeResources/drawable directory so that the same flags are available on all platforms:

    Compose Multiplatform resources project structure
  3. Build or run the application to generate the Res class with accessors for the added resources.

  4. Update the code in the commonMain/kotlin/.../App.kt file to support images:

    import demo.composeapp.generated.resources.jp import demo.composeapp.generated.resources.mx import demo.composeapp.generated.resources.eg import demo.composeapp.generated.resources.fr import demo.composeapp.generated.resources.id data class Country(val name: String, val zone: TimeZone, val image: DrawableResource) fun currentTimeAt(location: String, zone: TimeZone): String { fun LocalTime.formatted() = "$hour:$minute:$second" val time = Clock.System.now() val localTime = time.toLocalDateTime(zone).time return "The time in $location is ${localTime.formatted()}" } val defaultCountries = listOf( Country("Japan", TimeZone.of("Asia/Tokyo"), Res.drawable.jp), Country("France", TimeZone.of("Europe/Paris"), Res.drawable.fr), Country("Mexico", TimeZone.of("America/Mexico_City"), Res.drawable.mx), Country("Indonesia", TimeZone.of("Asia/Jakarta"), Res.drawable.id), Country("Egypt", TimeZone.of("Africa/Cairo"), Res.drawable.eg) ) @Composable @Preview fun App(countries: List<Country> = defaultCountries) { MaterialTheme { var showCountries by remember { mutableStateOf(false) } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) Row(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { DropdownMenu( expanded = showCountries, onDismissRequest = { showCountries = false } ) { countries.forEach { (name, zone, image) -> DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { Image( painterResource(image), modifier = Modifier.size(50.dp).padding(end = 10.dp), contentDescription = "$name flag" ) Text(name) } }, onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) } } } Button(modifier = Modifier.padding(start = 20.dp, top = 10.dp), onClick = { showCountries = !showCountries }) { Text("Select Location") } } } }

    • The Country type stores the path to the associated image.

    • The list of countries passed to the App includes these paths.

    • The App displays an Image in each DropdownMenuItem, followed by a Text composable with the name of a country.

    • Each Image requires a Painter object to fetch the data.

  5. Follow the IDE's instructions to import the missing dependencies.

  6. Run the application to see the new behavior:

The country flags in the Compose Multiplatform app on Android and iOS
The country flags in the Compose Multiplatform app on desktop
The country flags in the Compose Multiplatform app on the web

What's next

We encourage you to explore multiplatform development further and try out more projects:

Join the community:

15 May 2026