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:
In
composeApp/src/commonMain/kotlin
, open theApp.kt
file and replace the code with the followingApp
composable:@Composable @Preview 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 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 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:
Replace the current implementation of
App
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 { 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 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 on each platform you're targeting:
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()
, which you created earlier and which is no longer required.Follow the IDE's instructions to import the missing dependencies.
Adjust your
App
composable to invokecurrentTimeAt()
:@Composable @Preview 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") } } } }In the
wasmJsMain/kotlin/main.kt
file, add the following code before themain()
function to initialize timezone support for web:@JsModule("@js-joda/timezone") external object JsJodaTimeZoneModule private val jsJodaTz = JsJodaTimeZoneModuleRun the application again and enter a valid timezone.
Click 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 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)) { 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
and theTextField
.The
Text
composable fills the available horizontal space and centers its content.The
style
parameter customizes the appearance of theText
.
Follow the IDE's instructions to import the missing dependencies.
For
TextAlign
, use theandroidx.compose.ui.text.style
version.For
Alignment
, use theandroidx.compose.ui
version.
Run the application to see how the appearance has improved:
Refactor the design
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 preferable to ask users to select the country from a predefined 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 @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)) { 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
has replaced theTextField
. The value of theshowCountries
property determines the visibility of theDropdownMenu
. 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 through common code across all platforms. The Kotlin Multiplatform wizard has already added and configured this library, so you can start loading resources without having to modify the build file.
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.
Move the images to the
composeApp/src/commonMain/composeResources/drawable
directory so that the same flags are available on all platforms:Build or run the application to generate the
Res
class with accessors for the added resources.Update the code in the
commonMain/kotlin/.../App.kt
file to support images: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)) { 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 the
App
includes these paths.The
App
displays anImage
in eachDropdownMenuItem
, followed by aText
composable with the name of a country.Each
Image
requires aPainter
object to fetch the data.
Follow the IDE's instructions to import the missing dependencies.
Run the application to see the new behavior:
What's next
We encourage you to explore multiplatform development further and try out more projects:
Join the community:
Compose Multiplatform GitHub: star the repository and contribute
Kotlin Slack: Get an invitation and join the #multiplatform channel
Stack Overflow: Subscribe to the "kotlin-multiplatform" tag
Kotlin YouTube channel: Subscribe and watch videos about Kotlin Multiplatform