Spec-Zone.ru › Kotlin 2

Создайте собственное приложение

Теперь, когда вы изучили и улучшили пример проекта, созданный мастером, можно создать собственное приложение с нуля, используя уже знакомые концепции и познакомившись с новыми.

Вы создадите приложение «Местное время», в котором пользователи смогут указать страну и город, а приложение будет показывать время в столице этой страны. Вся функциональность приложения Compose Multiplatform будет реализована в общем коде с помощью мультиплатформенных библиотек. Приложение будет загружать и отображать изображения в выпадающем меню, а также использовать события, стили, темы, модификаторы и компоновки.

На каждом этапе можно запускать приложение на всех трёх платформах (iOS, Android и настольных компьютерах) или сосредоточиться на платформах, которые лучше всего соответствуют вашим потребностям.

Окончательную версию проекта можно найти в нашем репозитории GitHub.

Заложите основу

Для начала реализуйте новый компонуемый элемент App():

  1. В 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.

  2. Запустите приложение на Android и iOS:

    New Compose Multiplatform app on Android and iOS

    При запуске приложения и нажатии кнопки отобразится заданное в коде время — 13:30.

  3. Запустите приложение на компьютере с помощью Compose Hot Reload, запустив конфигурацию запуска «🔥desktopApp». Приложение работает, но окно не соответствует интерфейсу:

    New Compose Multiplatform app on desktop
  4. Чтобы исправить это, обновите файл 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 используется для установки начального размера и положения окна на экране.

  5. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.

  6. Чтобы приложение обновлялось автоматически, сохраните все изменённые файлы (⌘ S/Ctrl+S). Теперь его внешний вид должен улучшиться:

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

Добавьте ввод данных пользователем

Теперь добавьте возможность вводить название города, чтобы пользователи могли узнать время в этом месте. Проще всего это сделать, добавив компонуемый элемент TextField:

  1. Замените текущую реализацию 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.

  2. Следуйте рекомендациям IDE, чтобы импортировать недостающие зависимости.

  3. Запустите приложение на каждой целевой платформе. Отображаемое время по-прежнему задано в коде, но теперь в текстовое поле можно ввести часовой пояс:

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

Рассчитайте время

Следующий шаг — использовать введённые данные для расчёта времени. Для этого создайте функцию currentTimeAt():

  1. Вернитесь к файлу 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(), созданную ранее и больше не требующуюся.

    Если библиотека kotlinx-datetime ещё не добавлена в проект, следуйте инструкциям из раздела Добавление новой зависимости.

  2. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости. Убедитесь, что импортируете класс Clock из kotlin.time, а не из kotlinx.datetime.

  3. Измените компонуемый элемент 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")
               }
           }
       }
    }
    
  4. Снова запустите приложение и введите допустимый часовой пояс.

  5. Нажмите кнопку. Должно отобразиться правильное время:

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

Улучшите внешний вид

Приложение работает, но его внешний вид можно улучшить. Между компонуемыми элементами можно добавить больше пространства, а сообщение о времени сделать заметнее.

  1. Чтобы устранить эти недостатки, используйте следующую версию компонуемого элемента 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.

  2. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.

  3. Запустите приложение, чтобы увидеть, как улучшился его внешний вид:

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

Проведите рефакторинг интерфейса

Приложение работает, но в нём легко допустить опечатку. Например, если пользователь введёт «Франсе» вместо «Франция», приложение не сможет обработать этот ввод. Пользователям было бы удобнее выбирать страны из готового списка.

  1. Для этого обновите компонуемый элемент 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.

  2. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости. При импорте Row() выберите вариант @Composable.

  3. Запустите приложение, чтобы увидеть обновлённую версию:

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

Дополнительно улучшить архитектуру можно с помощью фреймворка внедрения зависимостей, например Koin, чтобы создать и внедрить таблицу местоположений. Если данные хранятся во внешнем источнике, для загрузки по сети можно использовать библиотеку Ktor, а для загрузки из базы данных — библиотеку SQLDelight.

Добавьте изображения

Список названий стран работает, но пользоваться им не очень удобно. Его можно улучшить, добавив изображения национальных флагов рядом с названиями стран.

Compose Multiplatform предоставляет библиотеку для доступа к ресурсам из общего кода на всех платформах. Мастер Kotlin Multiplatform уже добавил и настроил эту библиотеку, поэтому ресурсы можно загружать сразу.

Чтобы добавить изображения в проект, скачайте файлы изображений, сохраните их в нужном каталоге и добавьте код для их загрузки и отображения:

  1. Скачайте изображения флагов с сайта Flag CDN для стран из уже созданного вами списка. В данном случае это Япония, Франция, Мексика, Индонезия и Египет.

  2. Переместите изображения в каталог composeApp/src/commonMain/composeResources/drawable, чтобы одни и те же флаги были доступны на всех платформах:

    Compose Multiplatform resources project structure
  3. Соберите или запустите приложение, чтобы создать класс Res с геттерами для добавленных ресурсов.

  4. Обновите код в файле 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 для получения данных.

  5. Следуйте инструкциям IDE, чтобы импортировать недостающие зависимости.

  6. Запустите приложение, чтобы увидеть, как оно работает:

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

Окончательную версию проекта можно найти в нашем репозитории GitHub.

Что дальше

Предлагаем вам продолжить изучение мультиплатформенной разработки и попробовать другие проекты:

  • Сделайте приложение для Android кроссплатформенным

  • Создайте мультиплатформенное приложение с помощью Ktor и SQLDelight

  • Используйте общую бизнес-логику для iOS и Android, сохранив нативный интерфейс

  • Создайте приложение Compose Multiplatform с Kotlin/Wasm

  • Посмотрите подборку примеров проектов

Присоединяйтесь к сообществу:

  • GitHub Compose Multiplatform на GitHub: поставьте звезду репозиторию и участвуйте в разработке

  • Slack Kotlin в Slack: получите приглашение и присоединитесь к каналу #multiplatform

  • Stack Overflow Stack Overflow: подпишитесь на тег «kotlin-multiplatform»

  • YouTube YouTube-канал Kotlin: подпишитесь на канал и смотрите видео о Kotlin Multiplatform

10 июня 2026
Изменение проектаСоздайте приложение 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API