Kotlin Multiplatform Development Help

创建你自己的应用

现在你已经探索并改进了向导创建的示例项目,可以运用已掌握的概念并引入新知识,从头开始创建自己的应用了。

你将开发一个"本地时间应用",用户输入国家和城市后,应用会显示该国首都的时间。所有功能都将通过多平台库在共享代码中实现,包括下拉菜单中的图片加载显示,以及事件处理、样式、主题、修饰符和布局的运用。

每个阶段你都可以在三个平台(iOS、Android和桌面)上运行应用,也可以专注于最符合需求的特定平台。

奠定基础

首先实现新的App可组合项:

  1. composeApp/src/commonMain/kotlin中打开App.kt文件,替换为以下代码:

    @Composable @Preview fun App() { MaterialTheme { var timeAtLocation by remember { mutableStateOf("未选择地点") } Column { Text(timeAtLocation) Button(onClick = { timeAtLocation = "13:30" }) { Text("显示当地时刻") } } } }
    • 布局采用包含两个组件的纵向排列:顶部是Text组件,下方是Button

    • 这两个组件通过timeAtLocation状态属性实现联动, Text组件会实时响应状态变化

    • Button组件通过onClick事件处理器修改状态值

  2. 在Android和iOS平台运行应用:

    Android与iOS平台的新应用界面

    点击按钮后会显示预设的时间值。

  3. 桌面端运行效果可见窗口尺寸过大:

    桌面端初始运行效果
  4. composeApp/src/desktopMain/kotlin中修改main.kt文件:

    fun main() = application { val state = rememberWindowState( size = DpSize(400.dp, 250.dp), position = WindowPosition(300.dp, 300.dp) ) Window(title = "本地时间应用", onCloseRequest = ::exitApplication, state = state) { App() } }

    这里设置了窗口标题,并通过WindowState指定了初始尺寸和屏幕位置。

  5. 根据IDE提示导入缺失依赖项

  6. 再次运行桌面应用,显示效果已改善:

    优化后的桌面端界面

支持用户输入

现在让用户输入城市名称来查看当地时间。最简单的实现方式是添加TextField组件:

  1. 替换当前App实现:

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("未选择地点") } Column { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = "13:30" }) { Text("显示当地时刻") } } } }

    新增的TextFieldlocation属性绑定,用户输入时会通过onValueChange实时更新属性值。

  2. 根据IDE提示导入缺失依赖项

  3. 在各平台运行应用:

    Android与iOS平台的输入功能
    桌面端的输入功能

时间计算

接下来根据输入计算时间。创建currentTimeAt()函数:

  1. 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 "$location的当前时间是${localTime.formatted()}" } catch (ex: IllegalTimeZoneException) { null } }

    此函数与之前创建但已不再需要的todaysDate()函数类似。

  2. 导入缺失依赖项

  3. 修改App调用逻辑:

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("未选择地点") } Column { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = currentTimeAt(location) ?: "无效地点" }) { Text("显示当地时刻") } } } }
  4. wasmJsMain/kotlin/main.kt中添加时区支持:

    @JsModule("@js-joda/timezone") external object JsJodaTimeZoneModule private val jsJodaTz = JsJodaTimeZoneModule
  5. 运行应用并输入有效时区

  6. 点击按钮显示正确时间:

    Android与iOS平台的时间显示
    桌面端的时间显示

优化样式

当前界面存在间距和视觉层次问题,需要进行样式优化。

  1. 使用改进版App组件:

    @Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("未选择地点") } 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) ?: "无效地点" }) { Text("显示时间") } } } }
    • 通过modifier为各组件添加边距

    • Text组件占满宽度并居中显示

    • style参数定制文本外观

  2. 导入缺失依赖项

    • TextAlign使用androidx.compose.ui.text.style版本

    • Alignment使用androidx.compose.ui版本

  3. 查看优化后的界面:

    Android与iOS平台的样式优化
    桌面端的样式优化

重构设计

为避免用户输入错误,改为从预定义列表选择国家。

  1. 重构App组件设计:

    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 "$location的当前时间是${localTime.formatted()}" } fun countries() = listOf( Country("日本", TimeZone.of("Asia/Tokyo")), Country("法国", TimeZone.of("Europe/Paris")), Country("墨西哥", TimeZone.of("America/Mexico_City")), Country("印度尼西亚", TimeZone.of("Asia/Jakarta")), Country("埃及", TimeZone.of("Africa/Cairo")), ) @Composable @Preview fun App(countries: List<Country> = countries()) { MaterialTheme { var showCountries by remember { mutableStateOf(false) } var timeAtLocation by remember { mutableStateOf("未选择地点") } 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("选择地点") } } } }
    • 新增Country数据类型

    • currentTimeAt()函数增加时区参数

    • DropdownMenu替代TextField ,通过showCountries控制显隐

  2. 导入缺失依赖项

  3. 查看重构后的界面:

    Android与iOS平台的国家列表
    桌面端的国家列表

添加图片

用国旗图片替代文字列表可提升视觉效果。Compose Multiplatform已配置好跨平台资源访问功能。

  1. Flag CDN下载对应国家的国旗图片: 日本法国墨西哥印度尼西亚埃及

  2. 将图片放入composeApp/src/commonMain/composeResources/drawable目录:

    资源目录结构
  3. 构建项目生成资源访问类Res

  4. 更新代码支持图片显示:

    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 "$location的当前时间是${localTime.formatted()}" } val defaultCountries = listOf( Country("日本", TimeZone.of("Asia/Tokyo"), Res.drawable.jp), Country("法国", TimeZone.of("Europe/Paris"), Res.drawable.fr), Country("墨西哥", TimeZone.of("America/Mexico_City"), Res.drawable.mx), Country("印度尼西亚", TimeZone.of("Asia/Jakarta"), Res.drawable.id), Country("埃及", 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("未选择地点") } 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国旗" ) Text(name) } } } } } Button(modifier = Modifier.padding(start = 20.dp, top = 10.dp), onClick = { showCountries = !showCountries }) { Text("选择地点") } } } }

    • Country类型新增图片资源字段

    • 每个DropdownMenuItem中显示国旗图片和国家名称

    • Image组件通过Painter加载图片数据

  5. 导入缺失依赖项

  6. 查看最终效果:

    Android与iOS平台的国旗显示
    桌面端的国旗显示

下一步建议

继续探索多平台开发:

加入社区:

22 四月 2025