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, use events, styles, themes, modifiers, and layouts.
At each stage you can run the application on iOS, Android, and desktop, but you can focus on the platforms that best suit your needs.
Lay the foundation
To get started, implement a new App
composable:
In
composeApp/src/commonMain/kotlin
, open theApp.kt
file and replace the code with the followingApp
composable:@Composable fun App() { MaterialTheme { var timeAtLocation by remember { mutableStateOf("No location selected") } Column { 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 aButton
.The two composables are linked by a single piece of shared state, namely the
timeAtLocation
property. TheText
composable is an observer of this state.The
Button
composable changes the state using theonClick
event handler.
Run the application on Android and iOS:
When you run your application and click the button, the hardcoded time is displayed.
Run the application on the desktop. It works, but the window is clearly too large for the UI:
To fix this, in
composeApp/src/desktopMain/kotlin
, update themain.kt
file as follows:fun main() = application { val state = rememberWindowState( size = DpSize(400.dp, 250.dp), position = WindowPosition(300.dp, 300.dp) ) Window( title = "Local Time App", onCloseRequest = ::exitApplication, state = state ) { 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.Follow the IDE's instructions to import the missing dependencies.
Run the desktop application again. Its appearance should improve:
Support the user input
Now let users enter the name of the city to see the time at this location. The simplest way to achieve this is by adding a TextField
composable:
Replace the current implementation of
App
with the one below:@Composable fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = "Time at $location is 13:30" }) { Text("Show Time At Location") } } } }The new code adds both the
TextField
and alocation
property. As the user types into the text field, the value of the property is incrementally updated using theonValueChange
event handler.Follow the IDE's instructions to import the missing dependencies.
Run the application across the different platforms:
Calculate time
The next step is to use the given input to calculate time. To do this, create a currentTimeAt()
function:
Return to the
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()
that you created earlier, and which is now no longer required.Follow the IDE's instructions to import the missing dependencies.
Adjust your
App
composable to invokecurrentTimeAt()
:@Composable fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" }) { Text("Show Time At Location") } } } }Run the application again and enter a valid timezone.
Press the button, you should see the correct time:
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.
To address these issues, use this following version of the
App
composable:@Composable fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column(modifier = Modifier.padding(20.dp)) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() .align(Alignment.CenterHorizontally) ) TextField( value = location, modifier = Modifier.padding(top = 10.dp), onValueChange = { location = it } ) Button( modifier = Modifier.padding(top = 10.dp), onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" } ) { Text("Show Time") } } } }The
modifier
parameter adds padding all around theColumn
, as well as at the top of theButton
andTextField
.The
Text
composable fills the available horizontal space and centers its content.The
style
parameter customizes the content of theText
.
Follow the IDE's instructions to import the missing dependencies.
Run the application to see how the appearance has improved:
Refactor the design
The application works, but is prone to users' typos. It would be preferable to ask users to select an item from a list.
To achieve this, change the design in the
App
composable: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 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)) { 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( onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) { Text(name) } } } } 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 aTimeZone
as its second parameter.The
App
now requires a list of countries as a parameter. Thecountries()
function provides the list.DropdownMenu
replaced theTextField
. The value of theshowCountries
property determines the visibility ofDropdownMenu
. There is aDropdownMenuItem
for each country.
Follow the IDE's instructions to import the missing dependencies.
Run the application to see the redesigned version:
Introduce images
The list of country names works, but it's not visually appealing. You can improve it by replacing the names with images of national flags.
Compose Multiplatform provides a library for accessing resources in a common way across all platforms. The Kotlin Multiplatform wizard has already added and configured this library, so you don't need to modify the build file to start loading resources.
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:
Using an external resource, such as Flag CDN, download flags to match the list of countries you have already created. In this case, these are Japan, France, Mexico, Indonesia, and Egypt.
Place the images to
src/commonMain/resources
, so that the same flags can be available on all platforms:Change the codebase to support images:
data class Country(val name: String, val zone: TimeZone, val image: String) 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"), "jp.png"), Country("France", TimeZone.of("Europe/Paris"), "fr.png"), Country("Mexico", TimeZone.of("America/Mexico_City"), "mx.png"), Country("Indonesia", TimeZone.of("Asia/Jakarta"), "id.png"), Country("Egypt", TimeZone.of("Africa/Cairo"), "eg.png") ) @OptIn(ExperimentalResourceApi::class) @Composable 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)) { 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( onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) { Row(verticalAlignment = Alignment.CenterVertically) { Image( painterResource(image), modifier = Modifier.size(50.dp).padding(end = 10.dp), contentDescription = "$name flag" ) Text(name) } } } } } 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
App
includes these paths.The
App
displays anImage
in eachDropdownMenuItem
, followed by aText
that contains country names.Each
Image
requires aPainter
object to fetch the data.
Follow the IDE's instructions to import the missing dependencies and annotations.
Run the application to see the new behavior:
What's next
We encourage you to explore multiplatform development further and try out more projects: