Compose Multiplatform 實戰:CMP中跨平台Android、iOS程式碼的進入點
前言
Compose Multiplatform (簡稱CMP)
昨天我們大致瞭解了一下CMP的專案結構
我們從昨天的CMP的專案結構理解與編譯配置
中的專案結構可以知道
CMP專案可以在
commonMain
下寫共用邏輯
androidMain
下寫Android平台的邏輯
iosMain
下寫iOS平台的邏輯
desktopMain
下寫Desktop平台的邏輯
YourProjectName
├── composeApp
│ ├── ...
│ ├── src
│ │ ├── commonMain
│ │ ├── commonTest
│ │ ├── iosMain
│ │ └── desktopMain
│ └── ...
├── ...
├── ...
└── ...
接下來我們可以開始一步一步來理解
CMP
的程式碼進入點
因為涉及跨平台實作
所以總覺得需要好好理解下
程式碼是怎麼運作以及怎麼進到各自平台的
所以今天將詳細解說下CMP在跨平台的程式進入點
理解CMP程式的進入點
我們先簡單了解一下 Compose Multiplatform 跟 Kotlin Multiplatform
-
當然在CMP創建好的時候就已經幫你建立好這些進入點
這邊
僅需理解概念
即可 -
在CMP的commonMain下共用的程式碼進入點
預期
在androidMain、iOSMain..等跨平台程式碼都會去呼叫這個共用函示
來達到共用程式碼的目的
-
這裡建立了一個共用的App()函式
其中包含了
1.
自定義的通用UI Theme
2.使用了koin 注入viewmodel
3.自定義Compose UI的進入點
(這個後面章節會解釋如何自定義UI Theme、使用koin、自定義Compose UI...等主題)
// in ../commonMain/App.kt
@Composable
@Preview
fun App() {
ElegantAccessComposeTheme {
val viewModel = koinViewModel<MainViewModel>()
ElegantAccessApp(viewModel)
}
}
Android
實際呼叫commonMain中共用的App()函式
// in ../androidMain/App.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val androidModule = module {
single<Activity> { this@MainActivity }
single<Context> { this@MainActivity.applicationContext }
}
startKoin {
modules(appModule() + androidModule)
}
setContent {
// 呼叫剛剛實作共用的App()函式
App()
}
}
}
-
其中Android的
Android Manifest.xml
中會宣告這個MainActivity
的<activity>
tag以及開啟App的初始頁面
<intent-filter>
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application ...> <activity ... android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
iosMain
實際呼叫commonMain中共用的App()函式
// in ../commonIos/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
val uiViewController = LocalUIViewController.current
val iosModule = module {
single<UIViewControllerWrapper> { UIViewControllerWrapperImpl(uiViewController) }
}
KoinApplication(application = {
modules(appModule() + iosModule)
}) {
App()
}
}
- 實際在
iOS
會去呼叫上面MainViewController.kt
內的函式MainViewController()
-
desktopMain
中實際呼叫commonMain中共用的App()函式其中透過compose裡面的
application 函式
搭配Window
來完成desktop application
// in ../desktopMain/main.kt
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "App",
) {
App()
}
}
-
CMP中的desktop也是透過JVM去編譯
如果你要把他Build出來
則在環境中使用下方gradle cmd 即可
./gradlew desktopRun -DmainClass=MainKt --quiet
- 或者可以透過IDE直接把這個Gradle task加入到Run Configuration內
<img src="/images/compose/046.png" alt="Cover" style="width: 90%"/ class="prose-img">
開發共用邏輯
-
理解完上方進入點後
我們可以開始開發共用邏輯
來達到
只用一份Code
製作多個平台的應用程式 -
由下方圖片可看到
我們大部分的時間會花在
./commonMain
上主要的邏輯開發都在此
除了有些依賴各自平台的內容,例如:檔案系統、檔案選擇器...等等
才會透過
expect
跟autual
來實作(後面章節會也會再講怎麼用expect跟autual)
<img src="/images/compose/047.png" alt="Cover" style="width: 80%"/ class="prose-img">
-
不過到目前為止
即便是
desktop平台
或iOS平台
有自己的檔案系統導致在commonMain共用邏輯中需要自己實作
但在
KMM
或CMP
中都已經有支援透過
kotlin
程式碼來寫這些跨平台內容的Library了
你只需要在
Gradle中配置
即可
例如:透過Kotlin來實作desktop的檔案相關操作:
// ../desktop/PlatformFile.desktop.kt
actual class PlatformFile actual constructor(private val path: String) {
private val file = java.io.File(path)
actual fun exists() = file.exists()
actual fun createFile() = file.createNewFile()
actual fun writeBytes(bytes: ByteArray) = file.writeBytes(bytes)
actual fun delete() = file.delete()
actual fun isDirectory(): Boolean = file.isDirectory
actual fun copyTo(destination: PlatformFile, overwrite: Boolean) {
file.copyTo(java.io.File(destination.path), overwrite)
}
actual fun mkdirs() {
file.mkdirs()
}
}
actual class PlatformZip actual constructor() {
actual fun unzip(zipFilePath: String, destinationDir: String) {
java.util.zip.ZipFile(zipFilePath).use { zip ->
zip.entries().asSequence().forEach { entry ->
val file = java.io.File(destinationDir, entry.name)
if (entry.isDirectory) {
file.mkdirs()
} else {
file.parentFile.mkdirs()
zip.getInputStream(entry).use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
}
}
}