跟我一起使用 compose 做一个跨平台的黑白棋游戏(4)移植到compose-jb实现跨平台

1,840 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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:

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:

@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 是我自己定义的一个枚举类:

enum class Resource {
    WhiteChess,
    BlackChess,
    Background
}

这个枚举类定义了项目中用到的三个资源图片:白子图片、黑子图片、棋盘背景。

对了,为什么我之前的参数类型写的是 String 而现在要改成自定义枚举类,然后在实现中自己去解析?

哈哈,因为我实际写的时候才发现,由于界面代码写在了 commonMain 中,所以是没有 R 这个资源类的,也就是说我没法直接引用资源 ID,仔细想想也是,明明代码是放在平台无关的通用代码中,怎么可能会让使用安卓特有的 R 类呢。

所以,我们界面中加载图片的代码也要对应的改一下:

改之前:

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())

改之后:

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

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

desktop

actual fun chessboardSize(): Int {
    return 300
}

这里因为我们获取屏幕宽度的目的只是为了设置棋盘大小,所以对于桌面端我直接写死了一个值。

最后一个差异代码其实不用适配,但是由于我强迫症,不改总觉得不舒服,所以我还是改了。

那就是 Dialog 这个 composable ,在 jetpack compsoe 中,第一个必须参数的名字是 onDismissRequest 而在 compose-jb 中却叫做 onCloseRequest ……

其实在使用的时候不写参数名就可以不用适配了,但是我感觉不写不舒服,所以就得适配一下了:

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

android

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

desktop

@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