建立你自己的應用程式
本教學使用 IntelliJ IDEA,但你也可以在 Android Studio 中進行 —— 兩款 IDE 共享相同的核心功能與 Kotlin Multiplatform 支援。
這是 使用共享邏輯與 UI 建立 Compose Multiplatform 應用程式 教學的最後一部分。在繼續之前,請確保你已完成之前的步驟。
既然你已經探索並增強了由精靈建立的範例專案,現在你可以利用已掌握的概念並引入一些新概念,從頭開始建立你自己的應用程式。
你將建立一個「本地時間應用程式」,使用者可以在其中輸入國家和城市,應用程式將顯示該國首都的時間。你的 Compose Multiplatform 應用程式的所有功能都將使用多平台程式庫在共通程式碼中實作。它將在下拉式功能表中載入並顯示圖片,並將使用事件、樣式、佈景主題、修飾符和配置。
在每個階段,你都可以在所有三個平台(iOS、Android 與桌面)上執行應用程式,或者你可以專注於最符合你需求的特定平台。
你可以在我們的 GitHub 存儲庫 中找到專案的最終狀態。
奠定基礎
首先,實作一個新的 App() composable:
在
shared/src/commonMain/kotlin中,開啟App.kt檔案,並將程式碼替換為以下App()composable: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") } } } }- 配置是一個包含兩個 composable 的
Column。第一個是Textcomposable,第二個是Button。 - 這兩個 composable 透過單個共享狀態(即
timeAtLocation屬性)連結在一起。Textcomposable 是此狀態的觀察者。 Buttoncomposable 使用onClick事件處理常式來更改狀態。
- 配置是一個包含兩個 composable 的
在 Android 和 iOS 上執行應用程式:

當你執行應用程式並點擊按鈕時,會顯示硬編碼的時間 13:30。
使用 Compose Hot Reload,透過啟動 desktopApp [hot] 🔥 运行配置,在桌面平台上執行應用程式。 應用程式可以運作,但視窗顯然對於該 UI 而言太大了:

若要修正此問題,請按以下方式更新
desktopApp/src/kotlin目錄中的main.kt檔案:kotlinfun 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 的指示匯入缺失的相依性。
若要查看應用程式自動更新,請儲存任何修改過的檔案( / )。其外觀應該會有所改善:


支援使用者輸入
現在,讓使用者輸入城市名稱以查看該地點的時間。實現此功能最簡單的方法是新增一個 TextField composable:
將
commonMain/kotlin/compose.project.demo/App.kt中目前的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() 函式:
返回
shared/src/commonMain/kotlin/compose.project.demo/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()類似。如果專案尚未加入 kotlinx-datetime 程式庫,請按照 新增相依性 一節中的說明進行操作。
依照 IDE 的指示匯入缺失的相依性。 請確保從
kotlin.time匯入Clock類別,而不是從kotlinx.datetime匯入。調整你的
Appcomposable 以呼叫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") } } } }再次執行應用程式並輸入有效的時區。
點擊按鈕。你應該會看到正確的時間:



改進樣式
應用程式雖然可以運作,但外觀仍有一些問題。Composable 的間距可以更好,時間訊息的呈現也可以更醒目。
若要解決這些問題,請使用以下版本的
Appcomposable: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的頂部新增了 padding。Textcomposable 填滿可用的水平空間,並將其內容置中。style參數自訂了Text的外觀。
依照 IDE 的指示匯入缺失的相依性。
執行應用程式以查看外觀的改進:



重構 UI
應用程式雖然可以運作,但很容易出現拼寫錯誤。例如,如果使用者輸入「Franse」而不是「France」,應用程式將無法處理該輸入。更好的做法是要求使用者從預定義的清單中選擇國家。
若要實現此目的,請更新
App()composable 和currentTimeAt()函式,並新增一個輔助資料類別: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 的指示匯入缺失的相依性。 匯入
Row()時,請選擇@Composable版本。執行應用程式以查看重新設計後的版本:



你可以使用相依注入架構(例如 Koin)來進一步改進設計,以建置和注入位置表。如果資料儲存在外部,你可以使用 Ktor 程式庫透過網路獲取資料,或使用 SQLDelight 程式庫從資料庫中獲取資料。
導入圖片
國家名稱列表雖然可以運作,但視覺上不夠吸引人。你可以透過在國家名稱旁邊新增國旗圖片來改進清單。
Compose Multiplatform 提供了一個程式庫,用於在所有平台上透過共通程式碼存取資源。Kotlin Multiplatform 精靈已經新增並配置了此程式庫,因此你可以立即開始載入資源。
若要在專案中支援圖片,你需要下載圖片檔案,將它們儲存在正確的目錄中,並新增程式碼來載入和顯示它們:
從 Flag CDN 下載國旗圖片,以符合你已建立的國家列表。在這種情況下,分別是 日本、法國、墨西哥、印尼 和 埃及。
將圖片移至
composeApp/src/commonMain/composeResources/drawable目錄,以便在所有平台上都能使用相同的國旗:
建置或執行應用程式,以產生包含新增資源存取子(accessor)的
Res類別。更新
commonMain/kotlin/.../App.kt檔案中的程式碼以支援圖片:kotlinimport 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在每個DropdownMenuItem中顯示一個Image,後跟一個帶有國家名稱的Textcomposable。- 每個
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 的影片
