Compose大前端从上车到起飞

1,992 阅读8分钟

在前面的一篇文章赶紧上车开启Compose大前端之路中我们学习了CMP的基本概念以及如何创建一个CMP项目。今天将继续学习CMP,深入研究项目的配置方法,了解CMP的内部机制并总结一些CMP开发的最佳实践。

header

深入了解项目结构

CMP的基础结构与一个标准的Android应用的项目结构基本一样,只不过主module名字变成了「composeApp」。项目根目录下的build.gradle.kts是整个项目的配置,主要是指定使用的插件;settings.gradle.kts指定项目的依赖仓库,以及项目包含哪些module;gradle/wrapper/libs.version.toml定义着依赖的版本信息。模块「composeApp」包含着源码集合(source sets)以及一个build.gradle.kts,这是描述这个模块如何构建的脚本。

图1. 项目结构

对于Android开发同学来说,这样的项目结构非常熟悉,事实上如果把项目视图切换到「Android」,就会发现,这比起常规的Android项目,无非就是多了一些源码集合。

仔细看一下源码集合「androidMain」它就是一个标准的Android项目,里面有AndroidManifest文件,以及一个入口MainActivity,它会调用「commonMain」中定义的composable App(),由此就进入到了「commonMain」中。

android-entry.png

虽然并不在源码集合中,iosApp子目录其实是一个标准的Xcode项目,里面全是Xcode项目的配置文件,可以用Xcode直接打开。它是iOS应用的入口,它的调用顺序是iOSApp,到ContentView,这两个是标准的iOS的代码,用的是Swift。然后会进入到「iosMain」中的MainConntroller,这里就到了Kotlin地界了,MainController,再进入到common中的composable App(),由此进入了「commonMain」中。

ios-entry.png

依赖配置方法

现代的软件不可能全都是从零开始,有很多现成的代码库可以使用,这就需要为项目配置依赖。CMP中依赖配置方法与常规的Android略不一样,视依赖的使用,以及依赖的平台依赖性,需要分别针对不同的源码集合配置。

需要注意的就是依赖的作用域,如果是在commonMain中配置的,那就会对所有的平台生效;如果是为androidMain配置的依赖,只能在Android中生效,以次类推。

在源码集合中配置依赖

最直接的方式就是针对每个源码集合配置其依赖:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.example:my-library:1.0") // 所有平台共享的依赖
        }
    }
}

在顶层DSL dependencies中配置

除了在源码集合处配置依赖,也可以在顶层DSL dependencies中配置,本质都是一样的,只不过是方式略有不同。具体的格式是** <源码集合><具体的依赖> **,比如上面酱紫写:

dependencies {
    "commonMainImplementation"("com.example:my-library:1.0")
}

这与像上面在源码集合commonMain中配置是一样的。在顶层配置依赖的好处在于这里可以配置一些源码集合中找不到的依赖,如testing等等。

本地module如何相互依赖

如果是本地的库(module),可以通过project方式引入,同样的如果是共享的库加在common里,如果是某个平台特有的,或者只想在某个平台使用就单独加到它上面:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(project(":some-other-multiplatform-module"))
        }
    }
}

如何共享和定制代码

CMP的终极目的是要尽可能的共享代码,让一套代码能够跑在多个平台上面。但现实的世界是不完美的,平台的差异是不可忽视的。像存储,I/O,硬件资源等等都是平台强相关的。我们只能尽可能多的共享我们自己写的业务逻辑,对于实现业务逻辑而需要的系统平台相关的API,肯定还是需要每个平台定制化的去实现。

CMP通过Kotlin中的关键字expect和actual来处理平台定制API。

具体的做法就是在common中定义一个用关键字expect修饰的类型(函数,类,接口枚举,属性等),然后在每个平台的源码集合中去具体实现,并用关键字actual来修饰。注意,这里的类型没有限制,可以是函数,可以是类/接口/枚举,也可以是属性,尽管绝大多数情况下都是函数。

图4. 用expect和actual来定制API

可以理解为OO中的接口,但又不完全一样。区别在于,common中的expect函数不能有默认实现代码,并且函数的声明要在同一个包下面。编译的时候,编译器会用平台代码里面的actual去替换common中的expect函数。也就是说这是发生在编译时的行为,所以它要求包名和函数的签名完全一致。

资源管理

CMP是能构建跨平台UI的,而UI必然会涉及资源,最常见的资源就是图片和字符串,资源的复杂地方在于它会有限定属性,比如不同屏幕分辨率要用不同的图片,比如不同的地区语言要用不同的字符串,所以资源是相当复杂的,而且平台强相关。

为此CMP提供了一个专门的库用于管理资源,可以屏蔽平台特定,以统一的方式来管理资源。只需要在commonMain中引入依赖即可:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(compose.components.resources)
        }
    }
}

资源是放在commonMain中与kotlin同级别的目录中:

图5. 资源文件管理

资源可以分为几种类型,图片应该放在子目录drawable中,字体资源放在fonts中,字符串放在values中,其他文件放在files中。

资源文件还可以有限定符以支持定制化,如屏幕分辨率(xhdpi,xxhdpi等),区域语言(en,zh-rCN等)和主题(dark,light等)。可以发现,规则与Android平台资源管理规则是非常接近的。

图片字体等直接添加文件即可,字符串的话放在一个xml文件中,根Tag是resources,每个字符串的Tag是string,如:

<resources>
    <string name="app_name">My awesome app</string>
    <string name="title">Some title</string>
</resources>

需要注意资源的命名,与Android的资源规则一样,要是小写字符,可以有数字和下划线。

添加好资源后,编译插件会自动生成一个类Res,通过它就可以引用各种资源,如:

Image(
    painter = painterResource(Res.drawable.my_icon),
    contentDescription = null
)

Text(stringResource(Res.string.app_name))

一些开发实践建议

CMP是为了构建跨平台应用的,那么应该尽最大的可能去共享代码。虽然有平台定制机制,但只应该用它来定制细粒度的具体的API,而不是业务逻辑。比如说,从一个文件中读文本内容,不应该定义一个getFileContent,而是应该定制细粒度的openFile,closeFile和readLine,这是因为读取文件过程真正不同的是处理文件的API,除打开文件,读出每一行以外,其他的逻辑是一样的,应该共享。

还有,在写业务的时候要注意看Compose文档中以及Kotlin文档中API标注的平台范围,尽可能选择标记为「Common」或者「Cmn」的API。

另外,因为Kotlin是基于JVM的语言,标准JDK中的API都可以用,但在CMP的iOS(目标是Native)平台和Web(目标是Wasm)却无法使用JDK的API,所以我们应该多使用Kotlin标准库以及Kotlin扩展库(kotlinx),这些API都做了多平台适配。

总结

利益于Gradle中的源码集合,CMP对源码的结构是很宽容的,并没有严格的要求,这对于现有项目来说是相当友好的,因为把现有的项目源码搬进来就可以了,不用改太多,然后通过源码集合来做具体的指定和逻辑上的关系处理。每个源码集合其实都是其平台的一个标准项目,把common作为其依赖而已,关系就这么简单,甚至还可以用其平台的原生方式去写UI,写逻辑,这都是可以的。

虽然这样做貌似会失去共享代码和逻辑的意义,但是这对改造现有项目是十分友好的,比如第一步可能是把Android项目和iOS项目先融合进来,然后再慢慢的把两个平台的共享代码抽离出来入进common。这样做不但能慢慢推进跨端,每个项目各自仍是完整的,如果有紧急 的事情仍可先用原生方式去开发构建。

References

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!