Compose-Multiplatform在Android和iOS上的实践
Compose-Multiplatform目前虽然还不成熟,但通过对其原理的分析,我们可以预见的是,结合KMM,未来将成为跨平台的有力竞争者。特别对于Android开发同学来说,可以把KMM先用起来,结合Compose去实现一些低耦合的业务,待未来Compose-iOS发布稳定版后,可以愉快的进行双端开发,节约开发成本。
01简介
之前我们探讨过KMM,即Kotlin Multiplatform Mobile,是Kotlin发布的移动端跨平台框架。当时的结论是KMM提倡将共有的逻辑部分抽出,由KMM封装成Android(Kotlin/JVM)的aar和iOS(Kotlin/Native)的framework,再提供给View层进行调用,从而节约一部分的工作量。共享的是逻辑而不是UI。
其实在这个时候我们就知道Kotlin在移动端的跨平台绝对不是想止于逻辑层的共享,随着Compose的日渐成熟,JetBrains推出了Compose-Multiplatform,从UI层面上实现移动端,Web端,桌面端的跨平台。考虑到屏幕大小与交互方式的不同,Android和iOS之间的共享会极大的促进开发效率。比如现在已经非常成熟的Flutter。令人兴奋的是,Compose-Multiplatform目前已经发布了支持iOS系统的alpha版本,虽然还在开发实验阶段,但我们已经开始尝试用起来了。
02Jetpack-Compose与Compose-Multiplatform
作为Android开发,Jetpack-Compose我们再熟悉不过了,是Google针对Android推出的新一代声明式UI工具包,完全基于Kotlin打造,天然具备了跨平台的使用基础。JetBrains以Jetpack-Compose为基础,相继发布了compose-desktop,compose-web和compose-iOS,使Compose可以运行在更多不同平台,也就是我们今天要讲的Compose-Multiplatform。在通用的API上Compose-Multiplatform与Jetpack-Compose时刻保持一致,不同的只是包名发生了变化。因此作为Android开发,我们在使用Compose-Multiplatform时,可以将Jetpack-Compose代码低成本地迁移到Compose-Multiplatform,迁移过程较为直接。
03使用
既然是UI框架,那么我们就来实现一个简单的在移动端非常常规的业务需求:从服务器请求数据,并以列表形式展现在UI上。
在此我们要说明的是,Compose-Multiplatform是要与KMM配合使用的,其中KMM负责把shared模块编译成Android的aar和iOS的framework,Compose-Multiplatform负责UI层面的交互与绘制的实现。
首先我们先回顾一下KMM工程的组织架构:KMM工程通常包括androidApp、iosApp和shared模块,其中shared模块包含commonMain、androidMain和iosMain。commonMain为公共模块,代码与平台无关;androidMain和iosMain分别针对Android和iOS平台进行具体实现。
关于KMM工程的配置与使用方式,运行方式,编译过程原理请参考之前的文章,在此不做赘述。
接下来我们看Compose-Multiplatform是怎么基于KMM工程进行的实现。
1、添加配置
在settings.gradle文件中声明compose插件,其中compose.version在gradle.properties进行了声明。需要注意的是目前Compose-Multiplatform的版本有要求,目前可以参考官方的具体配置。
之后在shared模块的build.gradle文件中引用声明好的插件,同时配置compose静态资源文件的目录,方式如下:
- Android: 配置资源目录为src/commonMain/resources/
- iOS: 同样配置资源目录为src/commonMain/resources/
这意味着在寻找如图片等资源文件时,将从src/commonMain/resources/目录下寻找。由于目前compose-iOS还处于实验阶段,我们需要在gradle.properties文件中添加代码开启UIKit。最后我们需要为commonMain添加compose依赖。
配置完成后,开始写业务代码。既然是从服务器获取数据,我们肯定得封装一个网络模块,下面我们将使用ktor封装一个简单的网络模块。
2、网络模块
先在shared模块的build.gradle文件中添加ktor依赖。接下来封装一个最简单的HttpUtil,包含post和get请求;代码定义了HttpClient对象,进行基础设置来实现网络请求。然后定义接口请求返回的数据结构,例如一个数据类。
3、发送请求
定义SearchApi,实现search()方法发送网络请求。
4、View层的实现
创建SearchCompose,在searchCompose()里发送请求时开启一个协程,指定作用域。此外,定义ioDispatcher在不同平台下的实现:在Android上使用Dispatchers.IO,在iOS上使用相应的Dispatcher。在获取了服务端数据后,使用LazyColumn对列表进行实现,展示图片和文本。图片数据使用本地resources目录下的图片,文本展示服务端返回的数据。
5、图片加载
实现图片加载:创建一个ImageBitmap的remember对象,从resources目录下读取资源到内存中,然后在不同平台实现toImageBitmap()将它转换成Bitmap。toImageBitmap()的声明在commonMain,Android端使用Android特定API实现,iOS端使用iOS特定API实现。
在Android端,和Jetpack-Compose的调用方式一样,在MainActivity中直接调用SearchCompose。iOS端在shared模块定义main.ios文件,创建UIViewController对象MainViewController,作为iOS端和Compose链接的桥梁。在Android和iOS上都能正确显示列表内容。
04Android端的compose绘制原理
由于网上已经有很多Compose的相关绘制原理,这里进行简单的源码解析,来说明它是如何生成UI树并进行自绘的。Android端是在onCreate()里实现setContent()开始的,主要生成ComposeView并添加到DecorView中。ComposeView继承ViewGroup,通过setContent方法创建Composition对象和传入content函数。
LayoutNode是Compose渲染时的每个组件,最终组成LayoutNode树来描述UI界面。例如,创建一个Image时,会创建ComposeUiNode对象,其实现类是LayoutNode。Composition的作用是将LayoutNode组合起来,在页面生命周期状态下执行组合,创建父子依赖关系。
在AndroidComposeView中的dispatchDraw()实现了measureAndLayout()方法,为每个LayoutNode进行measure和layout。绘制工作由Painter完成,例如BitmapPainter在Canvas上进行绘制。最终调用自绘引擎skiko(Skia for Kotlin)在Canvas上进行绘制。Compose在移动端使用skiko引擎,与Flutter类似。
05Compose-Multiplatform与Flutter
在调研Compose-Multiplatform的过程中,我们发现它跟Flutter的原理类似,都是创建自己的View树,通过自绘引擎进行渲染。结合KMM,Compose-Multiplatform实现了逻辑和UI的共享,对于Android开发同学来说学习成本低。若Compose-Multiplatform更加成熟,发布稳定版后与Flutter的竞争会非常大。
06总结
Compose-Multiplatform目前虽然还不成熟,但通过对其原理的分析,我们可以预见的是,结合KMM,未来将成为跨平台的有力竞争者。特别对于Android开发同学来说,可以把KMM先用起来,结合Compose去实现一些低耦合的业务,待未来Compose-iOS发布稳定版后,可以愉快的进行双端开发,节约开发成本。
参考:
(1)https://www.jianshu.com/p/e1ae5eaa894e
(2) https://www.jianshu.com/p/e1ae5eaa894e
(3) https://github.com/JetBrains/compose-multiplatform-ios-android-template
(4) https://github.com/JetBrains/skiko