建立您的應用程式
本教學課程使用 IntelliJ IDEA,但您也可以在 Android Studio 中進行操作 – 這兩個 IDE 共享相同的核心功能並支援 Kotlin Multiplatform。
這是使用共享邏輯和使用者介面建立 Compose Multiplatform 應用程式教學課程的最後部分。在繼續之前,請確保您已完成先前的步驟。
現在您已經探索並增強了由精靈建立的範例專案,您可以使用您已知的概念並引入一些新概念,從頭開始建立您自己的應用程式。
您將建立一個「本地時間應用程式」,使用者可以在其中輸入他們的國家和城市,應用程式將顯示該國家首都的時間。您的 Compose Multiplatform 應用程式的所有功能都將使用多平台函式庫在通用程式碼中實作。它將在下拉式選單中載入並顯示影像,並將使用事件、樣式、主題、修改器和佈局。
在每個階段,您都可以在所有三個平台 (iOS、Android 和桌面) 上執行應用程式,或者您可以專注於最適合您需求的特定平台。
您可以在我們的 GitHub 儲存庫 中找到專案的最終狀態。
奠定基礎
首先,實作一個新的 App
可組合函式:
在
composeApp/src/commonMain/kotlin
中,開啟App.kt
檔案並將程式碼替換為以下App
可組合函式:kotlin@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 上執行應用程式:
當您執行應用程式並按一下按鈕時,將顯示硬編碼的時間。
在桌面上執行應用程式。它運作正常,但視窗對於使用者介面來說顯然太大了:
為了解決這個問題,請在
composeApp/src/desktopMain/kotlin
中,將main.kt
檔案更新如下:kotlinfun 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, alwaysOnTop = true ) { App() } }
在這裡,您設定了視窗標題並使用
WindowState
類型為視窗設定初始大小和螢幕位置。若要在桌面應用程式中即時查看您的變更,請使用 Compose 熱重載:
- 在
main.kt
檔案中,按一下邊槽中的 執行 圖示。 - 選擇 使用 Compose 熱重載 (Alpha) 執行 'main [desktop]'。
若要讓應用程式自動更新,請儲存任何修改過的檔案 ( / )。
Compose 熱重載目前處於 Alpha 階段,因此其功能可能會有所變更。
- 在
遵循 IDE 的指示匯入缺少的依賴項。
再次執行桌面應用程式。其外觀應會改善:
Compose 熱重載示範
支援使用者輸入
現在讓使用者輸入城市名稱以查看該位置的時間。最簡單的方法是新增一個 TextField
可組合函式:
將
App
的目前實作替換為以下內容:kotlin@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()
函式:
返回
App.kt
檔案並新增以下函式:kotlinfun 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 的指示匯入缺少的依賴項。
調整您的
App
可組合函式以呼叫currentTimeAt()
:kotlin@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") } } } }
在
wasmJsMain/kotlin/main.kt
檔案中,在main()
函式之前新增以下程式碼,以初始化對網路時區的支援:kotlin@JsModule("@js-joda/timezone") external object JsJodaTimeZoneModule private val jsJodaTz = JsJodaTimeZoneModule
再次執行應用程式並輸入有效的時區。
按一下按鈕。您應該會看到正確的時間:


改善樣式
應用程式正在運作,但在外觀上存在一些問題。可組合函式之間的間距可以更好,並且時間訊息可以更突出地呈現。
為了解決這些問題,請使用以下版本的
App
可組合函式:kotlin@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 的指示匯入缺少的依賴項。 對於
Alignment
,請使用androidx.compose.ui
版本。執行應用程式以查看外觀的改善:


重構設計
應用程式運作正常,但容易出現拼寫錯誤。例如,如果使用者輸入「Franse」而不是「France」,應用程式將無法處理該輸入。最好是要求使用者從預定義列表中選擇國家。
為此,請變更
App
可組合函式中的設計:kotlindata 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()
函式提供了此列表。DropdownMenu
已取代TextField
。showCountries
屬性的值決定了DropdownMenu
的可見性。每個國家都有一個DropdownMenuItem
。
- 有一個
遵循 IDE 的指示匯入缺少的依賴項。
執行應用程式以查看重新設計的版本:


您可以使用依賴注入框架,例如 Koin,進一步改善設計,以建構和注入位置表格。如果資料儲存在外部,您可以使用 Ktor 函式庫透過網路擷取,或使用 SQLDelight 函式庫從資料庫擷取。
引入影像
國家名稱列表運作正常,但視覺上不夠吸引人。您可以透過將名稱替換為國旗影像來改善它。
Compose Multiplatform 提供了一個函式庫,用於透過所有平台上的通用程式碼存取資源。Kotlin Multiplatform 精靈已經新增並配置了此函式庫,因此您可以開始載入資源,而無需修改建置檔案。
為了在專案中支援影像,您需要下載影像檔,將它們儲存在正確的目錄中,並新增程式碼來載入和顯示它們:
使用外部資源,例如 Flag CDN,下載符合您已建立的國家/地區列表的國旗。在此情況下,這些是 日本、法國、墨西哥、印尼 和 埃及。
將影像移動到
composeApp/src/commonMain/composeResources/drawable
目錄,以便所有平台都可使用相同的國旗:建置或執行應用程式以產生帶有新增資源存取器的
Res
類別。更新
commonMain/kotlin/.../App.kt
檔案中的程式碼以支援影像:kotlinimport compose.project.demo.generated.resources.eg import compose.project.demo.generated.resources.fr import compose.project.demo.generated.resources.id import compose.project.demo.generated.resources.jp import compose.project.demo.generated.resources.mx 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
在每個DropdownMenuItem
中顯示一個Image
,其後跟隨一個包含國家/地區名稱的Text
可組合函式。- 每個
Image
都需要一個Painter
物件來擷取資料。
遵循 IDE 的指示匯入缺少的依賴項。
執行應用程式以查看新行為:


您可以在我們的 GitHub 儲存庫 中找到專案的最終狀態。
後續步驟
我們鼓勵您進一步探索多平台開發並嘗試更多專案:
- 讓您的 Android 應用程式跨平台
- 使用 Ktor 和 SQLDelight 建立多平台應用程式
- 在 iOS 和 Android 之間共享業務邏輯同時保持原生 UI
- 使用 Kotlin/Wasm 建立 Compose Multiplatform 應用程式
- 查看精選範例專案列表
加入社群:
Compose Multiplatform GitHub:為 儲存庫 加星並貢獻
Kotlin Slack:獲取 邀請函 並加入 #multiplatform 頻道
Stack Overflow:訂閱 "kotlin-multiplatform" 標籤
Kotlin YouTube 頻道:訂閱並觀看有關 Kotlin Multiplatform 的影片