本文字数:4680字
预计阅读时间:30分钟
01
简介
02
Jetpack-Compose与
Compose-Multiplatform
03
使用
既然是UI框架,那么我们就来实现一个简单的在移动端非常常规的业务需求:
从服务器请求数据,并以列表形式展现在UI上。
关于kmm工程的配置与使用方式,运行方式,编译过程原理还是请回顾一下之前的文章,在此不做赘述。(2)
接下来我们看Compose-Multiplatform是怎么基于kmm工程进行的实现。
在settings.gradle文件中声明compose插件:
plugins{
//...
val composeVersion = extra["compose.version"] as String
id("org.jetbrains.compose").version(composeVersion)
}
#Versions
kotlin.version=1.8.20
agp.version=7.4.2
compose.version=1.4.0
之后在shared模块的build.gradle文件中引用声明好的插件如下:
plugins {
//...
id("org.jetbrains.compose")
}
android {
//...
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
}
iOS:
cocoapods {
//...
extraSpecAttributes["resources"] =
"['src/commonMain/resources/**', 'src/iosMain/resources/**']"
}
org.jetbrains.compose.experimental.uikit.enabled=true
最后我们需要在为commonMain添加compose依赖:
val commonMain by getting {
dependencies {
//...
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
// //implementation(compose.materialIconsExtended) // TODO not working on iOS for now
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(compose.ui)
}
}
先我们先在shared模块的build.gradle文件中添加依赖如下:
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:$ktor_version")//core
implementation("io.ktor:ktor-client-cio:$ktor_version")//CIO
implementation("io.ktor:ktor-client-logging:$ktor_version")//Logging
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")//Json格式化
//...
}
}
接下来我们封装一个最简单的HttpUtil,包含post和get请求;
package com.example.sharesample
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
class HttpUtil{
companion object{
val client: HttpClient = HttpClient(CIO) {
expectSuccess = true
engine {
maxConnectionsCount = 1000
requestTimeout = 30000
endpoint {
maxConnectionsPerRoute = 100
pipelineMaxSize = 20
keepAliveTime = 30000
connectTimeout = 30000
}
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
})
}
}
suspend inline fun <reified T> get(
url: String,//请求地址
): T? {
return try {
val response: HttpResponse = client.get(url) {//GET请求
contentType(ContentType.Application.Json)//content-type
}
val data: T = response.body()
data
} catch (e: ResponseException) {
print(e.response)
null
} catch (e: Exception) {
print(e.message)
null
}
}
suspend inline fun <reified T> post(
url: String,
): T? {//coroutines 中的IO线程
return try {
val response: HttpResponse = client.post(url) {//POST请求
contentType(ContentType.Application.Json)//content-type
}
val data: T = response.body()
data
} catch (e: ResponseException) {
print(e.response)
null
} catch (e: Exception) {
print(e.message)
null
}
}
}
}
package com.example.sharesample.bean
@kotlinx.serialization.Serializable
class SearchResult {
var count: Int? = null
var resInfos: List<ResInfoBean>? = null
}
package com.example.sharesample.bean
@kotlinx.serialization.Serializable
class ResInfoBean {
var name: String? = null
var desc: String? = null
}
接下来我们看看是怎么发送的请求。
然后我们定义个SearchApi:
package com.example.sharesample
import androidx.compose.material.Text
import androidx.compose.runtime.*
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.*
class SearchApi {
suspend fun search(): SearchResult {
Logger.SIMPLE.log("search2")
var result: SearchResult? =
HttpUtil.get(url = "http://h5-yapi.sns.sohuno.com/mock/229/api/v1/resInfo/search")
if (result == null) {
result = SearchResult()
}
return result
}
}
我们创建一个SearchCompose:
package com.example.sharesample
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource
class SearchCompose {
private val searchApi = SearchApi()
private var isInit = false
@OptIn(ExperimentalResourceApi::class)
@Composable
fun searchCompose() {
var searchResult by remember { mutableStateOf<SearchResult>(SearchResult()) }
if (!isInit) {
scope().launch {
val result = async {
searchApi.search()
}
searchResult = result.await()
}
isInit = true
}
Column {
Text(
"Total: ${searchResult.count ?: 0}",
style = TextStyle(fontSize = 20.sp),
modifier = Modifier.padding(start = 20.dp, top = 20.dp)
)
val scrollState = rememberLazyListState()
if (searchResult.resInfos != null) {
LazyColumn(
state = scrollState,
modifier = Modifier.padding(
top = 14.dp,
bottom = 50.dp,
end = 14.dp,
start = 14.dp
)
) {
items(searchResult.resInfos!!) { item ->
Box(
modifier = Modifier.padding(top = 20.dp).fillMaxWidth()
.background(color = Color.LightGray, shape = RoundedCornerShape(10.dp))
.padding(all = 20.dp)
) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
val picture = "1.jpg"
var imageBitmap: ImageBitmap? by remember(picture) {
mutableStateOf(
null
)
}
LaunchedEffect(picture) {
try {
imageBitmap =
resource(picture).readBytes().toImageBitmap()
} catch (e: Exception) {
}
}
if (imageBitmap != null) {
Image(
bitmap = imageBitmap!!, "", modifier = Modifier
.size(60.dp)
.clip(RoundedCornerShape(10.dp))
)
}
Text(
item.name ?: "name",
style = TextStyle(color = Color.Yellow),
modifier = Modifier.padding(start = 10.dp)
)
}
Text(item.desc ?: "desc", style = TextStyle(color = Color.White))
}
}
}
}
}
}
}
}
@Composable
fun scope(): CoroutineScope {
var viewScope = rememberCoroutineScope()
return remember {
CoroutineScope(SupervisorJob(viewScope.coroutineContext.job) + ioDispatcher)
}
}
expect val ioDispatcher: CoroutineDispatcher
在Android上的实现:
actual val ioDispatcher = Dispatchers.IO
在ios上的实现:
actual val ioDispatcher = Dispatchers.IO
具体的实现如下:
val picture = "1.jpg"
var imageBitmap: ImageBitmap? by remember(picture) {
mutableStateOf(
null
)
}
LaunchedEffect(picture) {
try {
imageBitmap =
resource(picture).readBytes().toImageBitmap()
} catch (e: Exception) {
}
}
if (imageBitmap != null) {
Image(
bitmap = imageBitmap!!, "", modifier = Modifier
.size(60.dp)
.clip(RoundedCornerShape(10.dp))
)
}
expect fun ByteArray.toImageBitmap(): ImageBitmap
Android端的实现:
fun ByteArray.toAndroidBitmap(): Bitmap {
return BitmapFactory.decodeByteArray(this, 0, size)
}
iOS端的实现:
actual fun ByteArray.toImageBitmap(): ImageBitmap =
Image.makeFromEncoded(this).toComposeImageBitmap()
好了通过以上的方式我们就可以实现对本地图片的加载,到此为止,Compose的相应实现就完成了。那么它是怎么被Android和ios的view引用的呢?Android端我们已经非常熟悉了,和Jetpack-Compose的调用方式一样,在MainActivity中直接调用即可:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SearchCompose().searchCompose()
}
}
}
}
}
ios端会稍微麻烦一点。我们先来看一下iosApp模块下iOSApp.swift的实现:
import UIKit
import shared
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let mainViewController = Main_iosKt.MainViewController()
window?.rootViewController = mainViewController
window?.makeKeyAndVisible()
return true
}
}
关键代码是这两行:
let mainViewController = Main_iosKt.MainViewController()
window?.rootViewController = mainViewController
package com.example.sharesample
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController
@Suppress("FunctionName", "unused")
fun MainViewController(): UIViewController =
ComposeUIViewController {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SearchCompose().searchCompose()
}
}
}
iOS端:
好了到此为止,我们看到了一个简单的列表业务逻辑是怎样实现的了。由于Compose-Multiplatform还未成熟,在业务实现上势必有很多内容需要自己造轮子。
04
Android端的compose绘制原理
Android端是在从onCreate()里实现setContent()开始的:
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SearchCompose().searchCompose()
}
}
}
setContent()的实现如下:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
if (inspectionWanted(owner)) {
owner.setTag(
R.id.inspection_slot_table_set,
Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
)
enableDebugInspectorInfo()
}
// 创建Composition对象,传入UiApplier
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
// 传入content函数
wrapped.setContent(content)
return wrapped
}
我们发现主要做了两件事情:
1.创建Composition对象,传入UiApplier
2.传入content函数
其中UiApplier的定义如下:
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root)
持有一个LayoutNode对象,它的说明如下:
An element in the layout hierarchy, built with compose UI
1)LayoutNode
我们假设创建一个Image,来看看Image的实现:
fun Image(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
//...
Layout(
{},
modifier.then(semantics).clipToBounds().paint(
painter,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter
)
) { _, constraints ->
layout(constraints.minWidth, constraints.minHeight) {}
}
}
继续追踪Layout()的实现:
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
},
skippableUpdate = materializerOf(modifier),
content = content
)
}
@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit,
noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
content: @Composable () -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
} else {
currentComposer.useNode()
}
Updater<T>(currentComposer).update()
SkippableUpdater<T>(currentComposer).skippableUpdate()
currentComposer.startReplaceableGroup(0x7ab4aae9)
content()
currentComposer.endReplaceableGroup()
currentComposer.endNode()
}
2)Composition
private class WrappedComposition(
val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver
我们来追踪一下它的setContent()的实现:
override fun setContent(content: @Composable () -> Unit) {
owner.setOnViewTreeOwnersAvailable {
if (!disposed) {
val lifecycle = it.lifecycleOwner.lifecycle
lastContent = content
if (addedToLifecycle == null) {
addedToLifecycle = lifecycle
// this will call ON_CREATE synchronously if we already created
lifecycle.addObserver(this)
} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
original.setContent {
@Suppress("UNCHECKED_CAST")
val inspectionTable =
owner.getTag(R.id.inspection_slot_table_set) as?
MutableSet<CompositionData>
?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
as? MutableSet<CompositionData>
if (inspectionTable != null) {
inspectionTable.add(currentComposer.compositionData)
currentComposer.collectParameterInformation()
}
LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }
CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
ProvideAndroidCompositionLocals(owner, content)
}
}
}
}
}
}
override fun setContent(content: @Composable () -> Unit) {
check(!disposed) { "The composition is disposed" }
this.composable = content
parent.composeInitial(this, composable)
}
3)Measure和Layout
在AndroidComposeView中的dispatchDraw()实现了measureAndLayout()方法:
override fun measureAndLayout(sendPointerUpdate: Boolean) {
trace("AndroidOwner:measureAndLayout") {
val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
if (rootNodeResized) {
requestLayout()
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
}
fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
var rootNodeResized = false
performMeasureAndLayout {
if (relayoutNodes.isNotEmpty()) {
relayoutNodes.popEach { layoutNode ->
val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode)
if (layoutNode === root && sizeChanged) {
rootNodeResized = true
}
}
onLayout?.invoke()
}
}
callOnLayoutCompletedListeners()
return rootNodeResized
}
4)绘制
我们还是以Image举例:
fun Image(
bitmap: ImageBitmap,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DefaultFilterQuality
) {
val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap, filterQuality = filterQuality) }
Image(
painter = bitmapPainter,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter
)
}
主要的绘制工作是由BitmapPainter完成的,它继承自Painter。
override fun DrawScope.onDraw() {
drawImage(
image,
srcOffset,
srcSize,
dstSize = IntSize(
this@onDraw.size.width.roundToInt(),
this@onDraw.size.height.roundToInt()
),
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
在onDraw()方法里实现了drawImage():
override fun drawImage(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
filterQuality: FilterQuality
) = drawParams.canvas.drawImageRect(
image,
srcOffset,
srcSize,
dstOffset,
dstSize,
configurePaint(null, style, alpha, colorFilter, blendMode, filterQuality)
)
05
Compose-Multiplatform与Flutter
06
总结