当前位置:   article > 正文

Compose-Multiplatform在Android和iOS上的实践

compose multiplatform

3a430cc1ca8fcfeb1c5dc3f573635579.jpeg

07415314dddc82d4bd5c1a785c54b30a.gif

本文字数:4680

预计阅读时间:30分钟

d1aadec1f609ed9f5752617e5d8c1526.png

01

简介

之前我们探讨过KMM,即Kotlin Multiplatform Mobile,是Kotlin发布的移动端跨平台框架。当时的结论是KMM提倡将共有的逻辑部分抽出,由KMM封装成Android(Kotlin/JVM)的aar和iOS(Kotlin/Native)的framework,再提供给View层进行调用,从而节约一部分的工作量。共享的是逻辑而不是UI。(1)

其实在这个时候我们就知道Kotlin在移动端的跨平台绝对不是想止于逻辑层的共享,随着Compose的日渐成熟,JetBrains推出了Compose-Multiplatform,从UI层面上实现移动端,Web端,桌面端的跨平台。考虑到屏幕大小与交互方式的不同,Android和iOS之间的共享会极大的促进开发效率。比如现在已经非常成熟的Flutter。令人兴奋的是,Compose-Multiplatform目前已经发布了支持iOS系统的alpha版本,虽然还在开发实验阶段,但我们已经开始尝试用起来了。

02

Jetpack-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:

6a97c7170dcc91f67267f35ac57af432.png

03

使用

既然是UI框架,那么我们就来实现一个简单的在移动端非常常规的业务需求:

从服务器请求数据,并以列表形式展现在UI上。

在此我们要说明的是,Compose-Multiplatform是要与KMM配合使用的,其中KMM负责把shared模块编译成Android的aar和iOS的framework,Compose-Multiplatform负责UI层面的交互与绘制的实现。

首先我们先回顾一下KMM工程的组织架构:

feb8cb45b15f17203e8328cc3535123a.png

其中androidApp和iosApp分别为Android和iOS这两个平台的主工程模块,shared为共享逻辑模块,供androidApp和iosApp调用。shared模块中:

  • commonMain为公共模块,该模块的代码与平台无关,是通过expected关键字对一些api的声明(声明的实现在platform module中);

  • androidMain和iosMain分别Android和ios这两个平台,通过actual关键字在平台模块进行具体的实现。

关于kmm工程的配置与使用方式,运行方式,编译过程原理还是请回顾一下之前的文章,在此不做赘述。(2)

接下来我们看Compose-Multiplatform是怎么基于kmm工程进行的实现。 

1、添加配置

在settings.gradle文件中声明compose插件:

  1. plugins{
  2. //...
  3.         val composeVersion = extra["compose.version"] as String
  4.         id("org.jetbrains.compose").version(composeVersion)
  5.     }

其中compose.version在gradle.properties进行了声明。需要注意的是目前Compose-Multiplatform的版本有要求,目前可以参考官方的具体配置。(3)

  1. #Versions
  2. kotlin.version=1.8.20
  3. agp.version=7.4.2
  4. compose.version=1.4.0

之后在shared模块的build.gradle文件中引用声明好的插件如下:

  1. plugins {
  2. //...
  3.     id("org.jetbrains.compose")
  4. }

同时我们需要在build.gradle文件中配置compose静态资源文件的目录,方式如下:

  • Android:

  1. android {
  2. //...
  3.     sourceSets["main"].resources.srcDirs("src/commonMain/resources")
  4. }
  • iOS:

  1. cocoapods {
  2. //...
  3.         extraSpecAttributes["resources"] =
  4.             "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
  5.     }

这意味着在寻找如图片等资源文件时,将从src/commonMain/resources/这个目录下寻找,如下图所示:

b66d899a78b29b1abcdc59c5b3fdc82e.jpeg

由于目前compose-iOS还处于实验阶段,我们需要在gradle.properties文件中添加如下代码开启UIKit:

org.jetbrains.compose.experimental.uikit.enabled=true

最后我们需要在为commonMain添加compose依赖:

  1. val commonMain by getting {
  2.             dependencies {
  3. //...
  4.                 implementation(compose.runtime)
  5.                 implementation(compose.foundation)
  6.                 implementation(compose.material)
  7. //                //implementation(compose.materialIconsExtended) // TODO not working on iOS for now
  8.                 @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
  9.                 implementation(compose.components.resources)
  10.                 implementation(compose.ui)
  11.             }
  12.         }

好了到此为止我们的配置就完成了,接下来开始写业务代码了。既然是从服务器获取数据,我们肯定得封装一个网络模块,下面我们将使用ktor封装一个简单的网络模块。 

2、网络模块

先我们先在shared模块的build.gradle文件中添加依赖如下:

  1. val commonMain by getting {
  2.             dependencies {
  3.                 implementation("io.ktor:ktor-client-core:$ktor_version")//core
  4.                 implementation("io.ktor:ktor-client-cio:$ktor_version")//CIO
  5.                 implementation("io.ktor:ktor-client-logging:$ktor_version")//Logging
  6.                 implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
  7.                 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")//Json格式化
  8. //...
  9.             }
  10.         }

接下来我们封装一个最简单的HttpUtil,包含post和get请求;

  1. package com.example.sharesample
  2. import io.ktor.client.*
  3. import io.ktor.client.call.*
  4. import io.ktor.client.engine.cio.*
  5. import io.ktor.client.plugins.*
  6. import io.ktor.client.plugins.contentnegotiation.*
  7. import io.ktor.client.plugins.logging.*
  8. import io.ktor.client.request.*
  9. import io.ktor.client.statement.*
  10. import io.ktor.http.*
  11. import io.ktor.serialization.kotlinx.json.*
  12. import kotlinx.coroutines.*
  13. import kotlinx.serialization.json.Json
  14. class HttpUtil{
  15.     companion object{
  16.         val client: HttpClient = HttpClient(CIO) {
  17.             expectSuccess = true
  18.             engine {
  19.                 maxConnectionsCount = 1000
  20.                 requestTimeout = 30000
  21.                 endpoint {
  22.                     maxConnectionsPerRoute = 100
  23.                     pipelineMaxSize = 20
  24.                     keepAliveTime = 30000
  25.                     connectTimeout = 30000
  26.                 }
  27.             }
  28.             install(Logging) {
  29.                 logger = Logger.DEFAULT
  30.                 level = LogLevel.HEADERS
  31.             }
  32.             install(ContentNegotiation) {
  33.                 json(Json {
  34.                     ignoreUnknownKeys = true
  35.                     isLenient = true
  36.                     encodeDefaults = false
  37.                 })
  38.             }
  39.         }
  40.         suspend inline fun <reified T> get(
  41.             url: String,//请求地址
  42.         ): T?  {
  43.             return try {
  44.                 val response: HttpResponse = client.get(url) {//GET请求
  45.                     contentType(ContentType.Application.Json)//content-type
  46.                 }
  47.                 val data: T = response.body()
  48.                 data
  49.             } catch (e: ResponseException) {
  50.                 print(e.response)
  51.                 null
  52.             } catch (e: Exception) {
  53.                 print(e.message)
  54.                 null
  55.             }
  56.         }
  57.         suspend inline fun <reified T> post(
  58.             url: String,
  59.         ): T?  {//coroutines 中的IO线程
  60.             return try {
  61.                 val response: HttpResponse = client.post(url) {//POST请求
  62.                     contentType(ContentType.Application.Json)//content-type
  63.                 }
  64.                 val data: T = response.body()
  65.                 data
  66.             } catch (e: ResponseException) {
  67.                 print(e.response)
  68.                 null
  69.             } catch (e: Exception) {
  70.                 print(e.message)
  71.                 null
  72.             }
  73.         }
  74.     }
  75. }

代码非常直观,定义了HttpClient对象,进行了基础的设置来实现网络请求。我们来定义一下接口请求返回的数据结构。

3、返回的数据结构

  1. package com.example.sharesample.bean
  2. @kotlinx.serialization.Serializable
  3. class SearchResult {
  4.     var count: Int? = null
  5.     var resInfos: List<ResInfoBean>? = null
  6. }
  1. package com.example.sharesample.bean
  2. @kotlinx.serialization.Serializable
  3. class ResInfoBean {
  4.     var name: String? = null
  5.     var desc: String? = null
  6. }

接下来我们看看是怎么发送的请求。

4、发送请求

然后我们定义个SearchApi:

  1. package com.example.sharesample
  2. import androidx.compose.material.Text
  3. import androidx.compose.runtime.*
  4. import com.example.sharesample.bean.SearchResult
  5. import io.ktor.client.plugins.logging.*
  6. import kotlinx.coroutines.*
  7. class SearchApi {
  8.     suspend fun search(): SearchResult {
  9.         Logger.SIMPLE.log("search2")
  10.         var result: SearchResult? =
  11.             HttpUtil.get(url = "http://h5-yapi.sns.sohuno.com/mock/229/api/v1/resInfo/search")
  12.         if (result == null) {
  13.             result = SearchResult()
  14.         }
  15.         return result
  16.     }
  17. }

实现了search()方法。接着我们来看view层的实现与数据的绑定是如何实现的。

5、View层的实现

我们创建一个SearchCompose:

  1. package com.example.sharesample
  2. import androidx.compose.foundation.Image
  3. import androidx.compose.foundation.background
  4. import androidx.compose.foundation.layout.*
  5. import androidx.compose.runtime.Composable
  6. import androidx.compose.foundation.lazy.LazyColumn
  7. import androidx.compose.foundation.lazy.items
  8. import androidx.compose.foundation.lazy.rememberLazyListState
  9. import androidx.compose.foundation.shape.RoundedCornerShape
  10. import androidx.compose.material.Text
  11. import androidx.compose.runtime.*
  12. import androidx.compose.ui.Alignment
  13. import androidx.compose.ui.Modifier
  14. import androidx.compose.ui.draw.clip
  15. import androidx.compose.ui.graphics.Color
  16. import androidx.compose.ui.graphics.ImageBitmap
  17. import androidx.compose.ui.text.TextStyle
  18. import androidx.compose.ui.unit.dp
  19. import androidx.compose.ui.unit.sp
  20. import com.example.sharesample.bean.SearchResult
  21. import io.ktor.client.plugins.logging.*
  22. import kotlinx.coroutines.CoroutineScope
  23. import kotlinx.coroutines.SupervisorJob
  24. import kotlinx.coroutines.async
  25. import kotlinx.coroutines.job
  26. import kotlinx.coroutines.launch
  27. import org.jetbrains.compose.resources.ExperimentalResourceApi
  28. import org.jetbrains.compose.resources.resource
  29. class SearchCompose {
  30.     private val searchApi = SearchApi()
  31.     private var isInit = false
  32.     @OptIn(ExperimentalResourceApi::class)
  33.     @Composable
  34.     fun searchCompose() {
  35.         var searchResult by remember { mutableStateOf<SearchResult>(SearchResult()) }
  36.         if (!isInit) {
  37.             scope().launch {
  38.                 val result = async {
  39.                     searchApi.search()
  40.                 }
  41.                 searchResult = result.await()
  42.             }
  43.             isInit = true
  44.         }
  45.         Column {
  46.             Text(
  47.                 "Total: ${searchResult.count ?: 0}",
  48.                 style = TextStyle(fontSize = 20.sp),
  49.                 modifier = Modifier.padding(start = 20.dp, top = 20.dp)
  50.             )
  51.             val scrollState = rememberLazyListState()
  52.             if (searchResult.resInfos != null) {
  53.                 LazyColumn(
  54.                     state = scrollState,
  55.                     modifier = Modifier.padding(
  56.                         top = 14.dp,
  57.                         bottom = 50.dp,
  58.                         end = 14.dp,
  59.                         start = 14.dp
  60.                     )
  61.                 ) {
  62.                     items(searchResult.resInfos!!) { item ->
  63.                         Box(
  64.                             modifier = Modifier.padding(top = 20.dp).fillMaxWidth()
  65.                                 .background(color = Color.LightGray, shape = RoundedCornerShape(10.dp))
  66.                                 .padding(all = 20.dp)
  67.                         ) {
  68.                             Column {
  69.                                 Row(verticalAlignment = Alignment.CenterVertically) {
  70.                                     val picture = "1.jpg"
  71.                                     var imageBitmap: ImageBitmap? by remember(picture) {
  72.                                         mutableStateOf(
  73.                                             null
  74.                                         )
  75.                                     }
  76.                                     LaunchedEffect(picture) {
  77.                                         try {
  78.                                             imageBitmap =
  79.                                                 resource(picture).readBytes().toImageBitmap()
  80.                                         } catch (e: Exception) {
  81.                                         }
  82.                                     }
  83.                                     if (imageBitmap != null) {
  84.                                         Image(
  85.                                             bitmap = imageBitmap!!, "", modifier = Modifier
  86.                                                 .size(60.dp)
  87.                                                 .clip(RoundedCornerShape(10.dp))
  88.                                         )
  89.                                     }
  90.                                     Text(
  91.                                         item.name ?: "name",
  92.                                         style = TextStyle(color = Color.Yellow),
  93.                                         modifier = Modifier.padding(start = 10.dp)
  94.                                     )
  95.                                 }
  96.                                 Text(item.desc ?: "desc", style = TextStyle(color = Color.White))
  97.                             }
  98.                         }
  99.                     }
  100.                 }
  101.             }
  102.         }
  103.     }
  104. }
  105. @Composable
  106. fun scope(): CoroutineScope {
  107.     var viewScope = rememberCoroutineScope()
  108.     return remember {
  109.         CoroutineScope(SupervisorJob(viewScope.coroutineContext.job) + ioDispatcher)
  110.     }
  111. }

在searchCompose()里我们看到了在发送请求时开启了一个协程,scope()方法指定了作用域,除此之外,我们还定义了ioDispatcher在不同平台下的实现,具体的声明如下:

expect val ioDispatcher: CoroutineDispatcher

在Android上的实现:

actual val ioDispatcher = Dispatchers.IO

在ios上的实现:

actual val ioDispatcher = Dispatchers.IO

需要注意的是,Android平台,Dispatchers.IO在jvmMain/Dispatchers,ios平台,Dispatchers.IO在nativeMain/Dispatchers下。两者是不一样的。在获取了服务端数据后,我们使用LazyColumn对列表进行实现。其中有图片和文本的展示。为了方便进行说明,图片数据我们使用本地resources目录下的图片,文本展示的是服务端返回的数据。下面我来说明一下图片的加载。

6、图片加载

具体的实现如下:

  1. val picture = "1.jpg"
  2. var imageBitmap: ImageBitmap? by remember(picture) {
  3.     mutableStateOf(
  4.         null
  5.     )
  6. }
  7. LaunchedEffect(picture) {
  8.     try {
  9.         imageBitmap =
  10.             resource(picture).readBytes().toImageBitmap()
  11.     } catch (e: Exception) {
  12.     }
  13. }
  14. if (imageBitmap != null) {
  15.     Image(
  16.         bitmap = imageBitmap!!, "", modifier = Modifier
  17.             .size(60.dp)
  18.             .clip(RoundedCornerShape(10.dp))
  19.     )
  20. }

先创建了一个ImageBitmap的remember对象,由于resource(picture).readBytes()是个挂起函数,我们需要用LaunchedEffect来执行。这段代码的作用是从resources目录下读取资源到内存中,然后我们在不同平台实现了toImageBitmap()将它转换成Bitmap。

  • toImageBitmap()的声明:

expect fun ByteArray.toImageBitmap(): ImageBitmap
  • Android端的实现:

  1. fun ByteArray.toAndroidBitmap(): Bitmap {
  2.     return BitmapFactory.decodeByteArray(this, 0, size)
  3. }
  • iOS端的实现:

  1. actual fun ByteArray.toImageBitmap(): ImageBitmap =
  2.     Image.makeFromEncoded(this).toComposeImageBitmap()

好了通过以上的方式我们就可以实现对本地图片的加载,到此为止,Compose的相应实现就完成了。那么它是怎么被Android和ios的view引用的呢?Android端我们已经非常熟悉了,和Jetpack-Compose的调用方式一样,在MainActivity中直接调用即可:

  1. class MainActivity : ComponentActivity() {
  2.     override fun onCreate(savedInstanceState: Bundle?) {
  3.         super.onCreate(savedInstanceState)
  4.         setContent {
  5.             MyApplicationTheme {
  6.                 Surface(
  7.                     modifier = Modifier.fillMaxSize(),
  8.                     color = MaterialTheme.colors.background
  9.                 ) {
  10.                     SearchCompose().searchCompose()
  11.                 }
  12.             }
  13.         }
  14.     }
  15. }

ios端会稍微麻烦一点。我们先来看一下iosApp模块下iOSApp.swift的实现:

  1. import UIKit
  2. import shared
  3. @UIApplicationMain
  4. class AppDelegate: UIResponder, UIApplicationDelegate {
  5.     var window: UIWindow?
  6.     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  7.         window = UIWindow(frame: UIScreen.main.bounds)
  8.         let mainViewController = Main_iosKt.MainViewController()
  9.         window?.rootViewController = mainViewController
  10.         window?.makeKeyAndVisible()
  11.         return true
  12.     }
  13. }

关键代码是这两行:

  1. let mainViewController = Main_iosKt.MainViewController()
  2.         window?.rootViewController = mainViewController

创建了一个MainViewController对象,然后赋给window的rootViewController。这个MainViewController是在哪儿怎么定义的呢?我们回到shared模块,定义一个main.ios的文件,它会在framework编译成Main_iosKt文件。main.ios的实现如下:

  1. package com.example.sharesample
  2. import androidx.compose.foundation.layout.fillMaxSize
  3. import androidx.compose.material.MaterialTheme
  4. import androidx.compose.material.Surface
  5. import androidx.compose.ui.Modifier
  6. import androidx.compose.ui.window.ComposeUIViewController
  7. import platform.UIKit.UIViewController
  8. @Suppress("FunctionName""unused")
  9. fun MainViewController(): UIViewController =
  10.     ComposeUIViewController {
  11.         MaterialTheme {
  12.             Surface(
  13.                 modifier = Modifier.fillMaxSize(),
  14.                 color = MaterialTheme.colors.background
  15.             ) {
  16.                 SearchCompose().searchCompose()
  17.             }
  18.         }
  19.     }

我们看到在这儿会创建一个UIViewController对象MainViewController。这个便是ios端和Compose链接的桥梁。接下来我们来看看在Android和ios上的效果。

  • Android端:

0aedcc3dc57efd58c80d97a68fb757a4.jpeg

  • iOS端:

a21c33ed37e07726876db3c066217c9b.png

好了到此为止,我们看到了一个简单的列表业务逻辑是怎样实现的了。由于Compose-Multiplatform还未成熟,在业务实现上势必有很多内容需要自己造轮子。 

04

Android端的compose绘制原理

由于网上已经有很多Compose的相关绘制原理,下一章我们只是进行简单的源码解析,来说明它是如何生成UI树并进行自绘的。

1、Android端的compose绘制原理

Android端是在从onCreate()里实现setContent()开始的:

  1. setContent {
  2.             MyApplicationTheme {
  3.                 Surface(
  4.                     modifier = Modifier.fillMaxSize(),
  5.                     color = MaterialTheme.colors.background
  6.                 ) {
  7.                     SearchCompose().searchCompose()
  8.                 }
  9.             }
  10.         }

setContent()的实现如下:

  1. public fun ComponentActivity.setContent(
  2.     parent: CompositionContext? = null,
  3.     content: @Composable () -> Unit
  4. ) {
  5.     val existingComposeView = window.decorView
  6.         .findViewById<ViewGroup>(android.R.id.content)
  7.         .getChildAt(0) as? ComposeView
  8.     if (existingComposeView != null) with(existingComposeView) {
  9.         setParentCompositionContext(parent)
  10.         setContent(content)
  11.     } else ComposeView(this).apply {
  12.         // Set content and parent **before** setContentView
  13.         // to have ComposeView create the composition on attach
  14.         setParentCompositionContext(parent)
  15.         setContent(content)
  16.         // Set the view tree owners before setting the content view so that the inflation process
  17.         // and attach listeners will see them already present
  18.         setOwners()
  19.         setContentView(this, DefaultActivityContentLayoutParams)
  20.     }
  21. }

我们看到它主要是生成了ComposeView然后通过setContent(content)将compose的内容注册到ComposeView里,其中ComposeView继承ViewGroup,然后调用ComponentActivity的setContentView()方法将ComposeView添加到DecorView中相应的子View中。通过追踪ComposeView的setContent方法:

  1. private fun doSetContent(
  2.     owner: AndroidComposeView,
  3.     parent: CompositionContext,
  4.     content: @Composable () -> Unit
  5. ): Composition {
  6.     if (inspectionWanted(owner)) {
  7.         owner.setTag(
  8.             R.id.inspection_slot_table_set,
  9.             Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
  10.         )
  11.         enableDebugInspectorInfo()
  12.     }
  13.    // 创建Composition对象,传入UiApplier
  14.     val original = Composition(UiApplier(owner.root), parent)
  15.     val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
  16.         as? WrappedComposition
  17.         ?: WrappedComposition(owner, original).also {
  18.             owner.view.setTag(R.id.wrapped_composition_tag, it)
  19.         }
  20.    // 传入content函数
  21.     wrapped.setContent(content)
  22.     return wrapped
  23. }

我们发现主要做了两件事情:

  • 1.创建Composition对象,传入UiApplier

  • 2.传入content函数

其中UiApplier的定义如下:

  1. internal class UiApplier(
  2.     root: LayoutNode
  3. ) : AbstractApplier<LayoutNode>(root)

持有一个LayoutNode对象,它的说明如下:

An element in the layout hierarchy, built with compose UI

可以看到LayoutNode就是在Compose渲染的时候,每一个组件就是一个LayoutNode,最终组成一个LayoutNode树,来描述UI界面。LayoutNode是怎么创建的呢?

1)LayoutNode

我们假设创建一个Image,来看看Image的实现:

  1. fun Image(
  2.     painter: Painter,
  3.     contentDescription: String?,
  4.     modifier: Modifier = Modifier,
  5.     alignment: Alignment = Alignment.Center,
  6.     contentScale: ContentScale = ContentScale.Fit,
  7.     alpha: Float = DefaultAlpha,
  8.     colorFilter: ColorFilter? = null
  9. ) {
  10. //...
  11.     Layout(
  12.         {},
  13.         modifier.then(semantics).clipToBounds().paint(
  14.             painter,
  15.             alignment = alignment,
  16.             contentScale = contentScale,
  17.             alpha = alpha,
  18.             colorFilter = colorFilter
  19.         )
  20.     ) { _, constraints ->
  21.         layout(constraints.minWidth, constraints.minHeight) {}
  22.     }
  23. }

继续追踪Layout()的实现:

  1. @Composable inline fun Layout(
  2.     content: @Composable @UiComposable () -> Unit,
  3.     modifier: Modifier = Modifier,
  4.     measurePolicy: MeasurePolicy
  5. ) {
  6.     val density = LocalDensity.current
  7.     val layoutDirection = LocalLayoutDirection.current
  8.     val viewConfiguration = LocalViewConfiguration.current
  9.     ReusableComposeNode<ComposeUiNode, Applier<Any>>(
  10.         factory = ComposeUiNode.Constructor,
  11.         update = {
  12.             set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
  13.             set(density, ComposeUiNode.SetDensity)
  14.             set(layoutDirection, ComposeUiNode.SetLayoutDirection)
  15.             set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
  16.         },
  17.         skippableUpdate = materializerOf(modifier),
  18.         content = content
  19.     )
  20. }
  21. @Composable @ExplicitGroupsComposable
  22. inline fun <T, reified E : Applier<*>> ReusableComposeNode(
  23.     noinline factory: () -> T,
  24.     update: @DisallowComposableCalls Updater<T>.() -> Unit,
  25.     noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
  26.     content: @Composable () -> Unit
  27. ) {
  28.     if (currentComposer.applier !is E) invalidApplier()
  29.     currentComposer.startReusableNode()
  30.     if (currentComposer.inserting) {
  31.         currentComposer.createNode(factory)
  32.     } else {
  33.         currentComposer.useNode()
  34.     }
  35.     Updater<T>(currentComposer).update()
  36.     SkippableUpdater<T>(currentComposer).skippableUpdate()
  37.     currentComposer.startReplaceableGroup(0x7ab4aae9)
  38.     content()
  39.     currentComposer.endReplaceableGroup()
  40.     currentComposer.endNode()
  41. }

在这里创建了ComposeUiNode对象,而LayoutNode就是ComposeUiNode的实现类。我们再来看看Composition。

2)Composition

从命名来看,Composition的作用就是将LayoutNode组合起来。其中WrappedComposition继承Composition:

  1. private class WrappedComposition(
  2.     val owner: AndroidComposeView,
  3.     val original: Composition
  4. ) : Composition, LifecycleEventObserver

我们来追踪一下它的setContent()的实现:

  1. override fun setContent(content: @Composable () -> Unit) {
  2.         owner.setOnViewTreeOwnersAvailable {
  3.             if (!disposed) {
  4.                 val lifecycle = it.lifecycleOwner.lifecycle
  5.                 lastContent = content
  6.                 if (addedToLifecycle == null) {
  7.                     addedToLifecycle = lifecycle
  8.                     // this will call ON_CREATE synchronously if we already created
  9.                     lifecycle.addObserver(this)
  10.                 } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
  11.                     original.setContent {
  12.                         @Suppress("UNCHECKED_CAST")
  13.                         val inspectionTable =
  14.                             owner.getTag(R.id.inspection_slot_table_set) as?
  15.                                 MutableSet<CompositionData>
  16.                                 ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
  17.                                     as? MutableSet<CompositionData>
  18.                         if (inspectionTable != null) {
  19.                             inspectionTable.add(currentComposer.compositionData)
  20.                             currentComposer.collectParameterInformation()
  21.                         }
  22.                         LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }
  23.                         CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
  24.                             ProvideAndroidCompositionLocals(owner, content)
  25.                         }
  26.                     }
  27.                 }
  28.             }
  29.         }
  30.     }

在页面的生命周期是CREATED的状态下,执行original.setContent():

  1. override fun setContent(content: @Composable () -> Unit) {
  2.         check(!disposed) { "The composition is disposed" }
  3.         this.composable = content
  4.         parent.composeInitial(this, composable)
  5.     }

调用parent的composeInitial()方法,这段代码我们就不再继续追踪下去了,它最终的作用就是对布局进行组合,创建父子依赖关系。 

3)Measure和Layout

在AndroidComposeView中的dispatchDraw()实现了measureAndLayout()方法:

  1. override fun measureAndLayout(sendPointerUpdate: Boolean) {
  2.         trace("AndroidOwner:measureAndLayout") {
  3.             val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
  4.             val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
  5.             if (rootNodeResized) {
  6.                 requestLayout()
  7.             }
  8.             measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
  9.         }
  10.     }
  11.     fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
  12.         var rootNodeResized = false
  13.         performMeasureAndLayout {
  14.             if (relayoutNodes.isNotEmpty()) {
  15.                 relayoutNodes.popEach { layoutNode ->
  16.                     val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode)
  17.                     if (layoutNode === root && sizeChanged) {
  18.                         rootNodeResized = true
  19.                     }
  20.                 }
  21.                 onLayout?.invoke()
  22.             }
  23.         }
  24.         callOnLayoutCompletedListeners()
  25.         return rootNodeResized
  26.     }

调用remeasureAndRelayoutIfNeeded,遍历relayoutNodes,为每一个LayoutNode去进行measure和layout。具体的实现不分析了。

4)绘制

我们还是以Image举例:

  1. fun Image(
  2.     bitmap: ImageBitmap,
  3.     contentDescription: String?,
  4.     modifier: Modifier = Modifier,
  5.     alignment: Alignment = Alignment.Center,
  6.     contentScale: ContentScale = ContentScale.Fit,
  7.     alpha: Float = DefaultAlpha,
  8.     colorFilter: ColorFilter? = null,
  9.     filterQuality: FilterQuality = DefaultFilterQuality
  10. ) {
  11.     val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap, filterQuality = filterQuality) }
  12.     Image(
  13.         painter = bitmapPainter,
  14.         contentDescription = contentDescription,
  15.         modifier = modifier,
  16.         alignment = alignment,
  17.         contentScale = contentScale,
  18.         alpha = alpha,
  19.         colorFilter = colorFilter
  20.     )
  21. }

主要的绘制工作是由BitmapPainter完成的,它继承自Painter。

  1. override fun DrawScope.onDraw() {
  2.         drawImage(
  3.             image,
  4.             srcOffset,
  5.             srcSize,
  6.             dstSize = IntSize(
  7.                 this@onDraw.size.width.roundToInt(),
  8.                 this@onDraw.size.height.roundToInt()
  9.             ),
  10.             alpha = alpha,
  11.             colorFilter = colorFilter,
  12.             filterQuality = filterQuality
  13.         )
  14.     }

在onDraw()方法里实现了drawImage():

  1. override fun drawImage(
  2.         image: ImageBitmap,
  3.         srcOffset: IntOffset,
  4.         srcSize: IntSize,
  5.         dstOffset: IntOffset,
  6.         dstSize: IntSize,
  7.         /*FloatRange(from = 0.0, to = 1.0)*/
  8.         alpha: Float,
  9.         style: DrawStyle,
  10.         colorFilter: ColorFilter?,
  11.         blendMode: BlendMode,
  12.         filterQuality: FilterQuality
  13.     ) = drawParams.canvas.drawImageRect(
  14.         image,
  15.         srcOffset,
  16.         srcSize,
  17.         dstOffset,
  18.         dstSize,
  19.         configurePaint(null, style, alpha, colorFilter, blendMode, filterQuality)
  20.     )

而最终也是在Canvas上进行了绘制。通过以上的分析,我们了解到Compose并不是和原生控件一一映射的关系,而是像Flutter一样,有自己的UI组织方式,并最终调用自绘引擎直接在Canvas上进行绘制的。在Android和iOS端使用的自绘引擎是skiko。这个skiko是什么呢?它其实是Skia for Kotlin的缩写(Flutter在移动端也是用的Skia引擎进行的绘制)。事实上不止是在移动端,我们可以通过以下的截图看到,Compose的桌面端和Web端的绘制实际上也是用了skiko:

6642bf8607f8ab57fa9d6c265ffeb2c2.png

关于skiko的更多信息,还请查阅文末的官方链接。(4)

好了到此为止,Compose的在Android端的绘制原理我们就讲完了。对其他端绘制感兴趣的同学可自行查看相应的源码,细节有不同,但理念都是一致的:创建自己的Compose树,并最终调用自绘引擎在Canvas上进行绘制。

05

Compose-Multiplatform与Flutter 

为什么要单拿它俩出来说一下呢?是因为在调研Compose-Multiplatform的过程中,我们发现它跟Flutter的原理类似,那未来可能就会有竞争,有竞争就意味着开发同学若在自己的项目中使用跨平台框架需要选择。那么我们来对比一下这两个框架:在之前KMM的文章中,我们比较过KMM和Flutter,结论是:

  • KMM主要实现的是共享逻辑,UI层的实现还是建议平台各自去处理;

  • Flutter是UI层的共享。

当时看来两者虽然都是跨平台,但目标不同,看上去并没有形成竞争。而在Compose-Multiplatform加入之后,结合KMM,成为了逻辑和UI都可以实现共享的结果。而且从绘制原理上来说,Compose和Flutter都是创建自己的View树,在通过自绘引擎进行渲染,原理上差异不大。再加上Kotlin和Compose作为Android的官方推荐,对于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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/906681
推荐阅读
相关标签
  

闽ICP备14008679号