创建你自己的应用
现在你已经探索并改进了向导创建的示例项目,可以运用已掌握的概念并引入新知识,从头开始创建自己的应用了。
你将开发一个"本地时间应用",用户输入国家和城市后,应用会显示该国首都的时间。所有功能都将通过多平台库在共享代码中实现,包括下拉菜单中的图片加载显示,以及事件处理、样式、主题、修饰符和布局的运用。
每个阶段你都可以在三个平台(iOS、Android和桌面)上运行应用,也可以专注于最符合需求的特定平台。
奠定基础
首先实现新的App
可组合项:
在
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
事件处理器修改状态值
在Android和iOS平台运行应用:
点击按钮后会显示预设的时间值。
桌面端运行效果可见窗口尺寸过大:
在
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
指定了初始尺寸和屏幕位置。根据IDE提示导入缺失依赖项
再次运行桌面应用,显示效果已改善:
支持用户输入
现在让用户输入城市名称来查看当地时间。最简单的实现方式是添加TextField
组件:
替换当前
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("显示当地时刻") } } } }新增的
TextField
与location
属性绑定,用户输入时会通过onValueChange
实时更新属性值。根据IDE提示导入缺失依赖项
在各平台运行应用:
时间计算
接下来根据输入计算时间。创建currentTimeAt()
函数:
在
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()
函数类似。导入缺失依赖项
修改
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("显示当地时刻") } } } }在
wasmJsMain/kotlin/main.kt
中添加时区支持:@JsModule("@js-joda/timezone") external object JsJodaTimeZoneModule private val jsJodaTz = JsJodaTimeZoneModule运行应用并输入有效时区
点击按钮显示正确时间:
优化样式
当前界面存在间距和视觉层次问题,需要进行样式优化。
使用改进版
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
参数定制文本外观
导入缺失依赖项
TextAlign
使用androidx.compose.ui.text.style
版本Alignment
使用androidx.compose.ui
版本
查看优化后的界面:
重构设计
为避免用户输入错误,改为从预定义列表选择国家。
重构
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
控制显隐
导入缺失依赖项
查看重构后的界面:
添加图片
用国旗图片替代文字列表可提升视觉效果。Compose Multiplatform已配置好跨平台资源访问功能。
将图片放入
composeApp/src/commonMain/composeResources/drawable
目录:构建项目生成资源访问类
Res
更新代码支持图片显示:
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
加载图片数据
导入缺失依赖项
查看最终效果:
下一步建议
继续探索多平台开发:
加入社区:
Compose Multiplatform GitHub :为仓库点赞并参与贡献
Kotlin Slack :获取邀请加入#multiplatform频道
Stack Overflow :关注"kotlin-multiplatform"标签
Kotlin YouTube频道 :订阅并观看Kotlin Multiplatform相关视频