跟我一起使用 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