初体验:实战Compose Multiplatform跨端(PC/Android)开发

1,812 阅读7分钟

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

前言

接上篇文章实战俄罗斯方块游戏(开发思路)快速上手使用了Compose开发俄罗斯方块后,在此基础上将其改成可在电脑端运行的PC应用

Compose Multiplatform 是 JetBrains 为桌面平台(macOS,Linux,Windows)和Web编写Kotlin UI框架

【最后效果】

1653658595(1).jpg 项目地址:github.com/chenjianche…

进入实战,首先根据官方文档搭建环境和学习Demo项目。附上官方Github地址: Compose Multiplatform

编译器

下载IntelliJ IDEA Community版

intellij IDEA

创建和了解Demo项目

IDEA下载完后,直接选择【新建项目】在新弹窗的左侧选择【Compose Multiplatform】,输入名称、项目保存位置、选择【Multiple platforms】,输入包名后,点击【创建】(看不清可点图片查看大图)

新建项目选择Compose Multiplatform
1653464063(1).jpg1653464205(1).jpg

这里创建的是名为demo的项目,创建完项目后,可能会报错

Caused by: com.android.builder.errors.EvalIssueException: SDK location not found. Define location with an ANDROID_SDK_ROOT environment variable or by setting the sdk.dir path in your project's local properties file at 'F:\Compose\demo\local.properties'.

这个是由于没有设置Android SDK,在编译器左上角选择【文件】-【设置】,在弹窗后将Android SDK location路径设置上即可

1653467065(1).jpg

工程结构

创建的Project里包含3个module:android、desktop、common。
common里src主要分了3个文件夹:commonMain、androidMain、desktopMain。

项目结构说明:

├─android 安卓项目
├─common  公共组件/代码
│  └─src
│      ├─androidMain android平台代码
│      ├─commonMain  公共代码
│      ├─desktopMain desktop平台代码
├─desktop PC端项目

1653470938(1).jpg

分别看一下3个module的gradle配置

/android安卓应用

/android 目录就是一个标准 Android 工程
android/build.gradle.kts

plugins {
    id("org.jetbrains.compose")
    id("com.android.application")
    kotlin("android")
}

repositories {
    jcenter()
}

dependencies {
    implementation(project(":common"))
    implementation("androidx.activity:activity-compose:1.4.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05")
}

android {
    compileSdkVersion(31)
    defaultConfig {
        applicationId = "com.example.android"
        minSdkVersion(24)
        targetSdkVersion(31)
        versionCode = 1
        versionName = "1.0-SNAPSHOT"
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
}

/desktop桌面应用

build.gradle.kts 指定入口文件、配置打包可分发的版本等

plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
}

kotlin {
    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "11"
        }
        withJava()
    }
    sourceSets {
        //依赖
        val jvmMain by getting {
            dependencies {
                implementation(project(":common"))
                implementation(compose.desktop.currentOs)
            }
        }
        val jvmTest by getting
    }
}

compose.desktop {
    application {
        mainClass = "MainKt" //入口文件
        nativeDistributions {
            //指定分发平台
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "demo"
            packageVersion = "1.0.0"
        }
    }
}

/common公共模块

build.gradle.kts ,其中使用 sourceSet 为 xxxMain 目录分别指定不同依赖,保证平台差异性:

plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
    id("com.android.library")
}

kotlin {
    android()
    jvm("desktop") {
        compilations.all {
            kotlinOptions.jvmTarget = "11"
        }
    }
    sourceSets {//不同目录分别指定不同依赖,以保证平台的差异性
        val commonMain by getting {
            dependencies {
                api(compose.runtime)
                api(compose.foundation)
                api(compose.material)
                @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
                api(compose.material3)
            }
        }
        //安卓平台
        val androidMain by getting {
            dependencies {
                api("androidx.appcompat:appcompat:1.4.1")
                api("androidx.core:core-ktx:1.7.0")
            }
        }
        //桌面平台
        val desktopMain by getting {
            dependencies {
                api(compose.preview)
            }
        }
    }
}

android {
    compileSdkVersion(31)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(24)
        targetSdkVersion(31)
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

Demo运行效果

先分别运行android和desktop项目查看效果:

运行android项目后,在安卓手机模拟器上的效果:

首页点击按钮后文字变为Hello Android
1653478168(1).jpg1653478210(1).jpg

运行desktop项目后,在window 10电脑上的效果:

首页点击按钮后文字变为Hello Desktop
1653478540(1).jpg1653478563(1).jpg

同窗效果: 1653478846(1).jpg

在不同环境平台上点击按钮后,会显示各自平台的文字(Android/Desktop),这是怎么做到的呢?接下来研究一下代码

/android项目里入口MainActivity代码

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                App()
            }
        }
    }
}

/desktop项目里入口Main.kt文件代码

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        App()
    }
}

/common项目里

@Composable
fun App() {
    var text by remember { mutableStateOf("Hello, World!") }
    val platformName = getPlatformName()

    Button(onClick = {
        text = "Hello, ${platformName}"
    }) {
        Text(text)
    }
}

android和desktop项目里都使用common项目里的可组合函数App(),其中有个函数getPlatformName(),这个和平台有个,查看一下定义:

expect fun getPlatformName(): String

kotlin expect关键字一般用在多平台上,比如在多平台项目中的common中声明方法签名,然后由不同的平台去实现该方法,从而实现一个多平台(跨平台)方法.

1653480994(1).jpg

1653480963(1).jpg

在文件夹androidMain、desktopMain下分别实现了getPlatformName()函数返回对应平台的字符串。

也就是涉及到平台差异性时,在commonMain里使用expect定义方法,在由对应平台下使用actual实现代码

运行/打包desktop项目

org.jetbrains.composeGradle插件简化了将应用程序打包到本机发行版中并在本地运行应用程序的过程,默认已经在desktop/build.gradle.kts引入了。

目前,该插件使用jpackage来打包可分发的应用程序。可分发应用程序是独立的、可安装的二进制文件,其中包括它们所需的所有 Java 运行时组件,而不需要在目标系统上安装 JDK

在编译器右侧选择Gradle,在项目下选择desktop->Tasks,选择run是本地直接运行项目,选择packageXXX是打包应用。packageDeb为Linux安装包,packageDmg为macos安装包,packageMsi为window安装包。
1653490842(1).jpg

打包window端安装包

双击点击【packageMsi】,等待执行完成之后,在项目路径下desktop\build\compose\binaries\main\msi\生成了1653493960(1).jpg程序(此时图标是默认图标)。

双击demo-1.0.0.msi运行安装包看看效果:

1653494090(1).jpg

1653494218(1).jpg

1653494390(1).jpg

安装过程中可能会出现安全管家拦截的情况,选择【允许程序操作】即可 1653494465(1).jpg

安装完成。

不过桌面并没有出现应用图标,应该是需要另外设置。查看一下安装后的文件夹,可以看到可执行文件demo.exe,双击后可正常运行demo应用。

1653494808(1).jpg

小结

通过IDEA编译器快速创建Compose Multiplatform项目,得到了默认包含android和desktop项目的工程目录,通过其内置的简单示例,了解了Compose Multiplatform项目结构,其默认配置能够正常打包出PC端的安装包。

迁移工作

清楚官方Compose Multiplatform项目结构后,就可以做迁移操作了,就是将现在的Android Compose项目里的各个@Compose可组合函数和各个公共代码迁移到common项目下,在由desktop项目去调用common的代码。

原单独安卓端的架构为MVI模式

  • View层:基于Compose打造,所有UI元素都由代码实现
  • Model层ViewModel维护State的变化,记录游戏状态
  • V-M通信:通过StateFlow驱动Compose刷新

涉及平台差异性的部分主要有:ViewModel 和 SoundPool(游戏声音播放)

ViewModel

安卓ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据,方便数据共享和减少内存泄漏。

通常的安卓里的ViewModel类定义方式:

class MainViewModel : ViewModel() {
    fun xxxFun() {
        //使用协程
        viewModelScope.launch {
        }
    }
}

需要改成纯kotlin方式,由外部传入CoroutineScope方式使用协程

open class MainViewModel {

    private var scope: CoroutineScope

    constructor(scope: CoroutineScope) {
        this.scope = scope
    }
    fun xxxFun() {
        scope.launch {
        }
    }
}

SoundPool

游戏音效播放,在/common/commonMain定义接口,在由android和desktop平台继承实现

interface SoundUtil {
    fun play(b: Boolean, sound: SoundType) {}
}

dp、px转换

在/common/commonMain定义后,在由/common/androidMain和/common/desktopMain各自实现

expect fun dp2px(dpValue: Float): Int
expect fun px2dp(pxValue: Float): Int

迁移前后项目结构对比

迁移前迁移后
1653802105(1).jpg1653802219(1).jpg

代码基本可以直接移动过去,除了上述提到的差异。

最后简单看一下desktop项目的Main.kt代码。在window方法里配置客户端名称、logo、及界面大小。创建MainViewModel来执行游戏逻辑,界面UI布局由Body(viewModel)绘制

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title = "俄罗斯方块", icon = painterResource("logo.png"), state = rememberWindowState(width = 480.dp, height = 860.dp)) {
        //游戏逻辑viewmodel
        val viewModel = MainViewModel(GlobalScope, null)
        LaunchedEffect(key1 = Unit) {
            //定时器,定时下落砖块
            while (isActive) {
                delay(650L - 55 * (viewModel.viewState.value.level - 1))
                if (viewModel.isGameRunning()) {
                    viewModel.move(Move.Down, false)
                    viewModel.checkMoveResult()
                }
            }
        }
        //界面UI布局
        Body(viewModel)
    }
}

最后

文章主要分享创建Compose Multiplatform开发跨端PC/Android项目的过程,通过分离平台相关代码,构建不涉及平台的纯kotlin的common项目,在由android或desktop项目依赖调用,可实现同一套代码运行出安卓和桌面端。另外通过org.jetbrains.composeGradle插件创建的Task,可一键打包出Linux、macos、window平台下的安装包。

对于安卓开发者来说日后一些小项目、小游戏,小工具通过Compose进行PC/Android跨端开发也是一个不错的选择。

感谢您的阅读~

项目地址:github.com/chenjianche…