跟我一起使用 compose 做一個跨平台的黑白棋遊戲(4)移植到compose-jb實現跨平台

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第6天,點擊查看活動詳情

前言

在上一篇文章中,我們已經實現了遊戲的所有界面和邏輯代碼,並且在 Android 上已經可以正常運行。

這篇文章我們將講解如何將其從使用 jetpack compose 修改為使用 compose-jb 從而實現跨平台。

老規矩,先看效果圖:

s1

可以看到,桌面端效果和移動端幾乎沒有差別,而且在移植過程中幾乎沒有修改代碼,幾乎就是直接複製過來就可以用了。

移植過程

準備工作

在開始之前,我們需要換一下 IDE,不再使用 Android Studio 而是改為使用 IntelliJ IDEA 。

其實這裏直接使用 Android Studio 也是完全沒問題,畢竟 Android Studio 本來就是魔改自 IDEA 社區版的。

而我之所以要換成 IDEA 只是因為 IDEA 的新建項目自帶了 Kotlin Multiplatform 模版,而這其中包括了 Compose Multiplatform 模版。

所以我可以直接使用模版創建項目,這樣就不用自己建一堆文件夾和文件了。

説簡單點其實就是為了偷懶,當然這裏説的是完全新建一個跨平台項目,如果你是直接 clone 我的項目或者其他 compose 跨平台項目那就沒必要非得用 IDEA。

如果你是新建項目,強烈建議還是使用 IDEA 的模版吧,不然自己手動創建容易出錯。

s2

選擇如上的項目模版後按照提示一步一步確定即可。

項目包結構

新建好項目後,項目的包結構如圖:

s3

其中,根目錄下 desktop 、 android 目錄分別為安卓和桌面端項目的原生目錄。

而 common 則為通用目錄,它下面又分了很多目錄:

androidMain 目錄是安卓的代碼(和資源)目錄,在編譯安卓程序時,其中的代碼和資源會被拷貝到根目錄下的 android 中。

desktopMain 同理,只不過這個是桌面端目錄。

而 commonMain 則是平台無關的通用代碼,無論編譯的是什麼平台都會參與編譯。

其他 *Test 是測試代碼目錄,咱們用不上就先不用管了。

複製代碼

知道了各個目錄的作用後,我們應該把代碼複製到哪兒已經顯而易見了。

咱們先把原本項目中的 gameLogic 、 gameView 、 viewModel 三個包中的文件全部複製到 common/src/commonMain/kotlin/包名 目錄下,複製完後結構如下:

s4

一般來説不會有什麼問題,因為新建項目時使用的模版已經幫我們把導入的依賴改好了。

雖然現在使用的代碼沒有變,但是實際上導入的包已經不是 jetpack compose 的包了。

如果複製文件過去後有什麼問題,按照提示改好即可。

複製資源

由於我們項目中使用到了一些圖片,所以需要我們把這些圖片分別複製到Android和desktoi的資源目錄中:

s5

android 的資源需要複製到 /common/src/androidMain/res 中,因為在安卓中我們使用的是 drawable 資源,所以需要我們在 res 目錄中新建一個 drawable 目錄,並把資源放到這個目錄中,這裏其實和原生安卓的資源一樣的。

desktop 的資源需要放到 /common/src/desktopMain/resources 目錄下。

適配差異代碼

其實在寫原生安卓程序的時候我們就説過,加載圖片的方式安卓和桌面端不一樣,所以需要我們單獨抽出一個函數,方便現在移植的時候修改。

當時我也以為這可能是這個項目中唯一有差異的地方,沒想到複製過來後又發現了兩處差異代碼,接下來就讓我們看看。

首先介紹一下對於平台差異代碼應該怎麼解決。

我們只需要在 commonMain 中用 expect 聲明一個函數,不要寫具體實現:

expect fun loadImageBitmap(resourceName: Resource): ImageBitmap

然後分別在 androidMain 和 desktopMain 中實現這個函數:

desktop:

```kotlin actual fun loadImageBitmap(resourceName: Resource): ImageBitmap { val resPath = when (resourceName) { Resource.WhiteChess -> "white_chess.png" Resource.BlackChess -> "black_chess.png" Resource.Background -> "mood.png" }

return useResource(resPath) { androidx.compose.ui.res.loadImageBitmap(it) }

} ```

android:

kotlin @Composable actual fun loadImageBitmap(resourceName: Resource): ImageBitmap { val resId = when (resourceName) { Resource.WhiteChess -> R.drawable.white_chess Resource.BlackChess -> R.drawable.black_chess Resource.Background -> R.drawable.mood } return ImageBitmap.imageResource(id = resId) }

其中的 Resource 是我自己定義的一個枚舉類:

kotlin enum class Resource { WhiteChess, BlackChess, Background }

這個枚舉類定義了項目中用到的三個資源圖片:白子圖片、黑子圖片、棋盤背景。

對了,為什麼我之前的參數類型寫的是 String 而現在要改成自定義枚舉類,然後在實現中自己去解析?

哈哈,因為我實際寫的時候才發現,由於界面代碼寫在了 commonMain 中,所以是沒有 R 這個資源類的,也就是説我沒法直接引用資源 ID,仔細想想也是,明明代碼是放在平台無關的通用代碼中,怎麼可能會讓使用安卓特有的 R 類呢。

所以,我們界面中加載圖片的代碼也要對應的改一下:

改之前:

kotlin val backgroundImage = loadImageBitmap(resourceName = R.drawable.mood.toString()) val whiteChess = loadImageBitmap(resourceName = R.drawable.white_chess.toString()) val blackChess = loadImageBitmap(resourceName = R.drawable.black_chess.toString())

改之後:

kotlin val backgroundImage = loadImageBitmap(resourceName = Resource.Background) val whiteChess = loadImageBitmap(resourceName = Resource.WhiteChess) val blackChess = loadImageBitmap(resourceName = Resource.BlackChess)

除此之外,還有一個地方的代碼也是需要適配一下,那就是獲取屏幕寬度:

val screenWidth = LocalConfiguration.current.screenWidthDp

之前我是萬萬沒想到,這個居然是安卓的特有代碼,仔細想想好像確實,這個代碼返回的是讀取系統配置文件的數據,桌面端確實沒有這個東西,而且桌面端的窗口大小是可變的啊。

所以我們需要改一下。

expect: expect fun chessboardSize(): Int

android

kotlin @Composable actual fun chessboardSize(): Int { return LocalConfiguration.current.screenWidthDp }

desktop

kotlin actual fun chessboardSize(): Int { return 300 }

這裏因為我們獲取屏幕寬度的目的只是為了設置棋盤大小,所以對於桌面端我直接寫死了一個值。

最後一個差異代碼其實不用適配,但是由於我強迫症,不改總覺得不舒服,所以我還是改了。

那就是 Dialog 這個 composable ,在 jetpack compsoe 中,第一個必須參數的名字是 onDismissRequest 而在 compose-jb 中卻叫做 onCloseRequest ……

其實在使用的時候不寫參數名就可以不用適配了,但是我感覺不寫不舒服,所以就得適配一下了:

expect: expect fun BaseDialog(onCloseRequest: () -> Unit, content: @Composable (() -> Unit))

android

kotlin @Composable actual fun BaseDialog( onCloseRequest: () -> Unit, content: @Composable () -> Unit ) { Dialog( onDismissRequest = onCloseRequest, content = content ) }

desktop

kotlin @Composable actual fun BaseDialog( onCloseRequest: () -> Unit, content: @Composable () -> Unit ) { Dialog( onCloseRequest = onCloseRequest, content = { content() } ) }

開始運行!

自此,移植就全部完成了!

我們來看一下運行效果。

桌面端:

在終端中輸入: ./gradlew run

或者依次選擇 Gradle - desktop - compose desktop - run

s6

移動端:

直接在菜單中運行即可

s7

運行效果:

s1

總結

截止到現在,我們終於完成了所有的界面和邏輯代碼,並且成功移植到了 compsoe-jb 實現了跨平台運行。

但是還有億些小細節需要我們好好的優化一下,這個就留到下一篇文章了,或者如果能寫的東西不多的話我就不再寫一篇新文章了,我就直接把更新代碼提交到 github 得了,所以歡迎大家 star 這個項目。

項目源碼地址:reversiChessCompose-Github