Создайте собственное приложение
Теперь, когда вы изучили и улучшили пример проекта, созданный мастером, можно создать собственное приложение с нуля, используя уже знакомые концепции и познакомившись с новыми.
Вы создадите приложение «Местное время», в котором пользователи смогут указать страну и город, а приложение будет показывать время в столице этой страны. Вся функциональность приложения Compose Multiplatform будет реализована в общем коде с помощью мультиплатформенных библиотек. Приложение будет загружать и отображать изображения в выпадающем меню, а также использовать события, стили, темы, модификаторы и компоновки.
На каждом этапе можно запускать приложение на всех трёх платформах (iOS, Android и настольных компьютерах) или сосредоточиться на платформах, которые лучше всего соответствуют вашим потребностям.
Заложите основу
Для начала реализуйте новый компонуемый элемент App():
-
В
shared/src/commonMain/kotlinоткройте файлApp.ktи замените код следующим компонуемым элементомApp():@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") } } } }Компоновка представляет собой колонку с двумя компонуемыми элементами. Первый — компонуемый элемент
Text, а второй —Button.Эти два компонуемых элемента связаны одним общим состоянием — свойством
timeAtLocation. Компонуемый элементTextнаблюдает за этим состоянием.Компонуемый элемент
Buttonизменяет состояние с помощью обработчика событияonClick.
-
Запустите приложение на Android и iOS:

При запуске приложения и нажатии кнопки отобразится заданное в коде время — 13:30.
-
Запустите приложение на компьютере с помощью Compose Hot Reload, запустив конфигурацию запуска «🔥desktopApp». Приложение работает, но окно не соответствует интерфейсу:
-
Чтобы исправить это, обновите файл
main.ktв исходном набореdesktopAppследующим образом: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() } }Здесь задаётся заголовок окна, а тип
WindowStateиспользуется для установки начального размера и положения окна на экране. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.
-
Чтобы приложение обновлялось автоматически, сохраните все изменённые файлы (⌘ S/Ctrl+S). Теперь его внешний вид должен улучшиться:
Добавьте ввод данных пользователем
Теперь добавьте возможность вводить название города, чтобы пользователи могли узнать время в этом месте. Проще всего это сделать, добавив компонуемый элемент TextField:
-
Замените текущую реализацию
App()вcommonMain/kotlin/compose.project.demo/App.ktна приведённую ниже:@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") } } } }Новый код добавляет как
TextField, так и свойствоlocation. По мере ввода текста в текстовое поле значение свойства обновляется с помощью обработчика событияonValueChange. Следуйте рекомендациям IDE, чтобы импортировать недостающие зависимости.
Запустите приложение на каждой целевой платформе. Отображаемое время по-прежнему задано в коде, но теперь в текстовое поле можно ввести часовой пояс:

Рассчитайте время
Следующий шаг — использовать введённые данные для расчёта времени. Для этого создайте функцию currentTimeAt():
-
Вернитесь к файлу
shared/src/commonMain/kotlin/compose.project.demo/App.ktи добавьте следующую функцию: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 } }Эта функция похожа на
todaysDate(), созданную ранее и больше не требующуюся. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости. Убедитесь, что импортируете класс
Clockизkotlin.time, а не изkotlinx.datetime.-
Измените компонуемый элемент
App, чтобы он вызывал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") } } } } Снова запустите приложение и введите допустимый часовой пояс.
Нажмите кнопку. Должно отобразиться правильное время:

Улучшите внешний вид
Приложение работает, но его внешний вид можно улучшить. Между компонуемыми элементами можно добавить больше пространства, а сообщение о времени сделать заметнее.
-
Чтобы устранить эти недостатки, используйте следующую версию компонуемого элемента
App:@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") } } } }Параметр
modifierдобавляет отступы вокругColumn, а также сверху уButtonиTextField.Компонуемый элемент
Textзанимает доступную горизонтальную область и выравнивает содержимое по центру.Параметр
styleнастраивает внешний видText.
Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.
Запустите приложение, чтобы увидеть, как улучшился его внешний вид:

Проведите рефакторинг интерфейса
Приложение работает, но в нём легко допустить опечатку. Например, если пользователь введёт «Франсе» вместо «Франция», приложение не сможет обработать этот ввод. Пользователям было бы удобнее выбирать страны из готового списка.
-
Для этого обновите компонуемый элемент
App()и функциюcurrentTimeAt(), добавив вспомогательный класс данных: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") } } } }Здесь есть тип
Country, состоящий из названия и часового пояса.Функция
currentTimeAt()принимаетTimeZoneв качестве второго параметра.Теперь для
Appтребуется список стран в качестве параметра. Этот список предоставляет функцияcountries().Вместо
TextFieldтеперь используетсяDropdownMenu. Значение свойстваshowCountriesопределяет видимостьDropdownMenu. Для каждой страны предусмотрен отдельныйDropdownMenuItem.
Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости. При импорте
Row()выберите вариант@Composable.Запустите приложение, чтобы увидеть обновлённую версию:

Добавьте изображения
Список названий стран работает, но пользоваться им не очень удобно. Его можно улучшить, добавив изображения национальных флагов рядом с названиями стран.
Compose Multiplatform предоставляет библиотеку для доступа к ресурсам из общего кода на всех платформах. Мастер Kotlin Multiplatform уже добавил и настроил эту библиотеку, поэтому ресурсы можно загружать сразу.
Чтобы добавить изображения в проект, скачайте файлы изображений, сохраните их в нужном каталоге и добавьте код для их загрузки и отображения:
Скачайте изображения флагов с сайта Flag CDN для стран из уже созданного вами списка. В данном случае это Япония, Франция, Мексика, Индонезия и Египет.
-
Переместите изображения в каталог
composeApp/src/commonMain/composeResources/drawable, чтобы одни и те же флаги были доступны на всех платформах: Соберите или запустите приложение, чтобы создать класс
Resс геттерами для добавленных ресурсов.-
Обновите код в файле
commonMain/kotlin/.../App.kt, добавив поддержку изображений: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") } } } }Тип
Countryхранит путь к соответствующему изображению.Список стран, передаваемый в
App, включает эти пути.Компонуемый элемент
AppотображаетImageв каждомDropdownMenuItem, а затем — компонуемый элементTextс названием страны.Для каждого
Imageтребуется объектPainterдля получения данных.
Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.
Запустите приложение, чтобы увидеть, как оно работает:

Что дальше
Предлагаем вам продолжить изучение мультиплатформенной разработки и попробовать другие проекты:
Создайте мультиплатформенное приложение с помощью Ktor и SQLDelight
Используйте общую бизнес-логику для iOS и Android, сохранив нативный интерфейс
Присоединяйтесь к сообществу:
Compose Multiplatform на GitHub: поставьте звезду репозиторию и участвуйте в разработке
Kotlin в Slack: получите приглашение и присоединитесь к каналу #multiplatform
Stack Overflow: подпишитесь на тег «kotlin-multiplatform»
YouTube-канал Kotlin: подпишитесь на канал и смотрите видео о Kotlin Multiplatform
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/compose-multiplatform-new-project.html
