浅析 JetPack Compose 是如何安装到View视图上

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 为什么 Compose 无需在意 view 层级问题,怎样嵌套都行? (最简单10s就能明白);

Hi , : )

看完本文可以帮你解开什么问题?

  • 为什么 Compose 无需在意 view 层级问题,怎样嵌套都行? (最简单10s就能明白);
  • Compose 如何安装到传统 View 视图上;

门外汉-从布局窥一眼

这是一段 Compose 的简单代码,我们演示了多层嵌套下的示例:


如果按照传统 View 的思维,我们不难发现,当前 content(R.id.content(FrameLayout)->) 布局中存在5层嵌套,这是极不可取的一种做法。

但是现在是 Compose ,最终的绘制真的会有5层吗?

我们打开 Filpper 看一下:

网络异常,图片无法展示
|

显然 R.id.content 下只有一个 ComposeView ,然后内部包含了一个 AndroidComposeView ,我们上述中的 Box 最终都被解析并安装到了这个自定义view上。

所以我们简单点可以总结为:

JetPack-Compose 其自定义了一个 基础容器- ComposeView ,以及其他扩展View,比如 AndroidComposeView ,并对其进行封装,对外提供了各种我们在上层所使用的各种组件或者容器。

所以当我们在 Compose 中 setContent 后,其初始化了一个 ComposeView ,并且添加了一个 AndroidComposeView ,其承载了我们代码中所写的全部组件,并进行解析,最终绘制在了传统UI中。


所以为什么说Compose不在意布局层级呢?


因为人家只有两层啊,即业务代码中,ComposeView 下就只有一个 AndroidComposeView ,而其他 Image,Box 等组件都是人家自己绘制的。你说相比 传统View 还会存在层级问题吗😂


一些猜测:


为什么叫 AndroidComposeView 呢?


Compose 现在不仅仅支持 Android,现在预览版也支持 Desktop ,所以很可能 ComposeView 很可能还会涉及其他平台系统。所以最顶层是 ComposeView ,而对于 Android 的支持为 AndroidComposeView

当然上述只是我一个猜测,如果大佬们有其他想法,也欢迎分享。

解析-setContent内部实现

我们在上面知道了 Compose 最终在 Android View 的展现形式,那么它到底是怎样设置上去的呢,接下来我们就简单解析一下,不涉及Compose 相关过多源码,比较好理解:

ComponentActivity

setContent

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    //获取decorView->R.id.content->内第一个view,在compose中,默认为composeView
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView
    👉 -> 如果view不为null,则证明已经安装过,则重新设置内容
    if (existingComposeView != null) with(existingComposeView) {
        //设置父Compose内容
        setParentCompositionContext(parent)
        //设置当前页面内容
        setContent(content)
    👉 -> 如果上面获取的view是空,也就是现在还没有安装,则生成一个新的去安装到 R.id.content 上
    } else ComposeView(this).apply {
        //设置父Compose内容
        setParentCompositionContext(parent)
        //设置当前页面内容
        setContent(content)
        //设置TreeLifecycleOwner,实际上是修复Appcompat1.3及以下的bug,忽视即可
        setOwners()
        //设置内容视图,activity的方法
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

从上面的源码,我们的下一步主要关注点为:

  • setParentCompositionContext()
  • setContent()

ComposeView

setParentCompositionContext

设置视图合成的父级 context ,里面仅仅是一个赋值,暂时跳过

fun setParentCompositionContext(parent: CompositionContext?) {
        parentContext = parent
}

setContent

设置 compose UI 内容,当视图被添加到窗口时调用。

fun setContent(content: @Composable () -> Unit) {
    shouldCreateCompositionOnAttachedToWindow = true
    this.content.value = content
    //如果已经挂接到窗口,即view完全显示时,isAttachedToWindow=true
    //第一次调用时,这里为false,所以当onAttachToWindows调用时,下面的方法才会被调用
    if (isAttachedToWindow) {
        createComposition()
    }
}

我们接下来去看看 AbstractComposeViewonAttachToWindows() 方法。

AbstractComposeView

onAttachToWindows

当被安装到windows上时调用。

 override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        previousAttachedWindowToken = windowToken
        //这里当onAttached时为true,因为上一步已经赋值为true
        if (shouldCreateCompositionOnAttachedToWindow) {
            ensureCompositionCreated()
        }
    }
ensureCompositionCreated
private fun ensureCompositionCreated() {
    //整个流程中,我们没看见composition的初始化,所以这里为null
    if (composition == null) {
        try {
            👉 1.  ///结合步骤1来看,就是一个用于判断是否add过的变量
            creatingComposition = true
            👉 2. // 先看 resolveParentCompositionContext() -> 得到一个顶级CompositionContext
            👉 3. // 拿着parentContext,传入setContent方法
            composition = setContent(resolveParentCompositionContext()) {
              Content()
            }
        } finally {
            creatingComposition = false
        }
    }
}

resolveParentxxxContext

其作用是解析父组合上下文。

private fun resolveParentCompositionContext() = parentContext
    //👉1. 
    ?: findViewTreeCompositionContext()?.also { cachedViewTreeCompositionContext = it }
    ?: cachedViewTreeCompositionContext
    // 👉2. 
    ?: windowRecomposer.also { cachedViewTreeCompositionContext = it }
1. findViewTreexxxContext

查找当前view树的父context

fun View.findViewTreeCompositionContext(): CompositionContext? { 
  //先取自己的compositionContenxt,如果取到直接返回,否则不断向上找父级的context.
var found: CompositionContext? = compositionContext
if (found != null) return found
var parent: ViewParent? = parent
while (found == null && parent is View) {
  found = parent.compositionContext
  parent = parent.getParent()
}
return found
}

View.compositionContext

compositionContext 是一个扩展函数,内部使用tag保存当前context

var View.compositionContext: CompositionContext?

get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext

set(value) {

setTag(R.id.androidx_compose_ui_view_composition_context, value)

}

2. windowRecomposer

我们接着上面的流程继续分析:

@OptIn(InternalComposeUiApi::class)
internal val View.windowRecomposer: Recomposer
    get() {
        ...
        //这个rootView就是R.id.content对应的FrameLayout的第一个子view,即ComposeView
        val rootView = contentChild
        // 因为上述是初始化状态,所以 rootParentRef 第一次也为null,所以我们继续往下看
        return when (val rootParentRef = rootView.compositionContext) {
            //调用窗口Recomposer创建一个新的,并且把根content传入进去
            null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)
            ...
        }
    }

View.contentChild

private val View.contentChild: View
get() {
  var self: View = this
  var parent: ViewParent? = self.parent
  while (parent is View) {
      if (parent.id == android.R.id.content) return self
      self = parent
      parent = self.parent
  }
  return self
}
createAndxxxRecomposer(rootView)

创建一个 Recomposer,并且将其赋值给rootView的扩展变量 compositionContext

internal fun createAndInstallWindowRecomposer(rootView: View): Recomposer {
        //使用默认的工厂创建一个Recomposer
        val newRecomposer = factory.get().createRecomposer(rootView)
        //将其赋值给rootView的compositionContext,而compositionContext也是一个扩展函数,通用使用tag保存
        rootView.compositionContext = newRecomposer
      ...
}
-- View.compositionContext
var View.compositionContext: CompositionContext?
get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext
set(value) {
  setTag(R.id.androidx_compose_ui_view_composition_context, value)
}

在这里,我们知道了resolveParentCompositionContext() 实际上是初始化了 ComposeView 的扩展变量 compositionContext 。并且我们得到了这个返回值 parentContext

ViewGroup.setContent

接着我们继续回到setContent中,去看看:

👉3. setContent(resolveParentCompositionContext())

internal fun ViewGroup.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
  if (childCount > 0) {
      //第一次调用这里肯定为null,注意下面方法调用
      getChildAt(0) as? AndroidComposeView
  } else {
      removeAllViews(); null
    //初始化一个AndroidComposeView,并将我们最外面的content函数传入,
      //将初始化的AndroidComposeView添加到ComposeView上
  } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
  //这里留一个伏笔,即我们的下个阶段将
return doSetContent(composeView, parent, content)
}

如上所述,这里拿到当前 viewGroup 即 ComposeView ,然后判断 childCount 否有子view,因为是第一次,所以肯定执行到了 flase.

即执行到了 AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) } ,也就是生成了一个AndroidComposeView 的 ViewGroup 容器,内部构造函数中的context 正是我们第三步的 content() ,也就是我们自己的业务代码。然后调用 ComposeView 的 addView() 方法,将自己添加到 ComposeView 中。

到这里为止,如果你还记得我们最开始的布局层级,那就应该能明白最基础的流程。

网络异常,图片无法展示
|

总结

1.当我们调用 Compsoe 的 setContent() 之后,其内部先判断当前的基础 (R.id.content) 的 View 是不是 ComposeView ,如果不是则初始化一个,并且调用其的 setContent() 方法,将 shouldCreateCompositionOnAttachedToWindow 置为 true ,并且将最开始传入的 content 函数赋值给 Compose 内部的变量 content 。

2.接着使用 Activity 的 setContentView() ,将初始化的 ComposeView 添加到底层布局 R.id.content 上;

3.在 view 完全可见时,即 onAttachView 被调用时,开始去初始化当前 compose 页面,其内部初始化了一个名为 AndroidComposeView 的子View。然后调用我们传入的 content() 函数,生成一个 content ,将其作为构造函数传入 AndroidComposeView 中,从而生成了子view。然后将其 add 到了 ComposeView 上。从而完成了布局的初始化。

碎碎念

本文是理解 Compose 设计中比较简单的一篇,适合初学的同学简单了解 Compose与View 的相爱相杀。后续我将继续深追 Compose 的部分源码设计以及在实际落地中的场景解决方案。

目录
相关文章
|
3月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
83 4
|
5月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
6月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
6月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
6月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
6月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
6月前
深入了解 Jetpack Compose 中的 Modifier
深入了解 Jetpack Compose 中的 Modifier
113 0
|
6月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
|
6月前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
|
6月前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用