29、Compose 与原生 View 互操作:AndroidView、ComposeView

说实话,在2025年的今天,纯 Compose 项目已经越来越多了。但现实总是骨感的——你手头大概率还有一堆写好的原生 View 代码,或者要用到某个还没出 Compose 版本的第三方库。这时候怎么办?

嗯,Google 早就想到了。Compose 和原生 View 之间可以互相嵌套,这就是我们今天要聊的互操作。说白了,就是让新老代码能和平共处,慢慢过渡。

为什么需要互操作?

我接手过一个项目,里面有个自定义地图控件,写了上千行 Canvas 绘制逻辑。要是全部重写成 Compose,那得加班到明年。但新功能又想在 Compose 里写,怎么办?

互操作的核心场景就这几个:

  • 渐进式迁移:老项目不可能一夜之间全改 Compose,得一块一块来
  • 复用成熟组件:比如 WebView、MapView、VideoView,这些在 Compose 里还没完美替代品
  • 性能敏感区域:某些高频刷新的自定义 View,用 Compose 重写反而可能掉帧
  • 团队协作:有人熟悉 View 体系,有人熟悉 Compose,互操作让两边能一起干活

核心原则:Compose 和 View 可以互相嵌套,但尽量保持单向依赖。我个人习惯是让 Compose 作为容器,里面嵌入 View,这样更符合声明式 UI 的思维。

在 Compose 里用原生 View:AndroidView

AndroidView 是个 Composable 函数,它允许你把一个原生 View 塞进 Compose 的组件树里。用法其实很简单:

@Composable
fun MapViewInCompose(
    modifier: Modifier = Modifier
) {
    // 记得在 AndroidManifest 里申请 MAPS_API_KEY
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            MapView(context).apply {
                // 这里做初始化配置
                onCreate(null)
                onResume()
                // 设置地图中心点
                getMapAsync { googleMap ->
                    googleMap.uiSettings.isZoomControlsEnabled = true
                }
            }
        },
        update = { mapView ->
            // 当重组发生时,这里会被调用
            // 可以用来更新 View 的状态
        }
    )
}

这里有几个关键点我得提醒你:

  • factory 只执行一次:它只在首次组合时调用,用来创建 View 实例
  • update 每次重组都会执行:所以别在里面做耗时操作
  • 生命周期要自己管:像 MapView 这种有生命周期的组件,你得在合适的时机调用 onResume()onPause()

小技巧:如果你需要在 Compose 里监听 View 的回调,可以用 DisposableEffect 来注册和注销监听器。我在项目里就是这么处理 WebView 的 WebViewClient 的。

在原生 View 里用 Compose:ComposeView

反过来,如果你有一个旧的 XML 布局,想在里面嵌入 Compose 组件,那就用 ComposeView。它本质上是个 ViewGroup,里面跑了一个 Compose 的宿主。

在 XML 里这样声明:

<androidx.compose.ui.platform.ComposeView
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

然后在 Activity 或 Fragment 里绑定:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<ComposeView>(R.id.compose_view).setContent {
            // 这里就可以写 Compose 代码了
            MaterialTheme {
                Greeting("来自 Compose 的问候")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

你想想看,这相当于在旧的 View 体系里开了一个 Compose 的「窗口」。我一般用它来做渐进式迁移——先把某个复杂的 Fragment 里的内容用 ComposeView 替换,然后逐步把里面的 View 换成 Compose 组件。

生命周期与状态同步

互操作最头疼的问题是什么?生命周期和状态同步。我曾经踩过一个坑:Compose 里的状态变了,但嵌入的 View 没更新,结果界面显示错乱。

解决方案其实不复杂:

@Composable
fun SyncExample() {
    var counter by remember { mutableStateOf(0) }

    AndroidView(
        factory = { context ->
            Button(context).apply {
                text = "点击次数: 0"
                setOnClickListener {
                    // 这里不能直接修改 counter,因为是在 View 的线程
                    // 需要通过回调来更新 Compose 状态
                }
            }
        },
        update = { button ->
            // 每次 counter 变化,这里都会执行
            button.text = "点击次数: $counter"
        }
    )

    Button(onClick = { counter++ }) {
        Text("增加计数")
    }
}

注意看 update 参数——它就像一座桥,把 Compose 的状态和 View 的属性连接起来。只要 Compose 里的 counter 变了,update 就会自动触发,更新 Button 的文字。

警告:不要在 factory 里直接读取 Compose 的状态!因为 factory 只执行一次,后续状态变化它感知不到。所有状态驱动的更新,都应该放在 update 里做。

性能考量与最佳实践

互操作不是没有代价的。每次 Compose 和 View 之间跨边界传递数据,都会有一定的性能开销。我做过测试,频繁的跨边界更新会导致掉帧。

几个实战建议:

  • 减少跨边界次数:能一次传多个数据,就别拆成多次
  • 避免在 update 里做重操作:它每次重组都会执行,里面别 new 对象、别做 IO
  • 用 remember 缓存 View 引用:如果需要在 Compose 外部操作 View,记得用 remember 把 View 实例存下来
  • 考虑用 Modifier 替代直接操作 View:能用 Compose 的 Modifier 实现的布局效果,就别去改 View 的 LayoutParams
// 推荐的做法:用 remember 缓存 View 引用
@Composable
fun CachedViewExample() {
    val viewRef = remember { mutableStateOf<MyCustomView?>(null) }

    AndroidView(
        factory = { context ->
            MyCustomView(context).also { viewRef.value = it }
        }
    )

    // 其他地方可以通过 viewRef.value 访问 View
    // 但要注意线程安全
}

知识体系总览

下面这张图帮你理清 Compose 与原生 View 互操作的完整脉络:

Compose 与原生 View 互操作知识体系 Compose 世界 原生 View 世界 互操作桥梁 AndroidView ComposeView AndroidView 要点 • factory 只执行一次 • update 每次重组执行 • 生命周期需手动管理 • 状态通过 update 同步 ComposeView 要点 • 本质是 ViewGroup • 在 XML 中声明 • setContent 绑定 Compose • 适合渐进式迁移 最佳实践 • 减少跨边界次数 • 避免 update 中重操作 • 用 remember 缓存 View • 优先用 Modifier

实战:封装一个可复用的 WebView 组件

最后,我分享一个实际项目中用到的封装。WebView 在 Compose 里没有原生替代品,所以必须用 AndroidView 来包装:

@Composable
fun rememberWebViewState(url: String): WebViewState {
    return remember(url) {
        WebViewState(url)
    }
}

class WebViewState(val initialUrl: String) {
    var isLoading by mutableStateOf(false)
    var progress by mutableIntStateOf(0)
    var title by mutableStateOf("")
    var webView: WebView? = null
}

@Composable
fun WebViewComponent(
    url: String,
    modifier: Modifier = Modifier,
    state: WebViewState = rememberWebViewState(url)
) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            WebView(context).apply {
                settings.javaScriptEnabled = true
                settings.domStorageEnabled = true
                
                webViewClient = object : WebViewClient() {
                    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                        state.isLoading = true
                        state.title = url ?: ""
                    }
                    override fun onPageFinished(view: WebView?, url: String?) {
                        state.isLoading = false
                        state.title = view?.title ?: ""
                    }
                }
                
                webChromeClient = object : WebChromeClient() {
                    override fun onProgressChanged(view: WebView?, newProgress: Int) {
                        state.progress = newProgress
                    }
                }
                
                loadUrl(url)
                state.webView = this
            }
        },
        update = { webView ->
            if (webView.url != url) {
                webView.loadUrl(url)
            }
        }
    )
}

这个封装的好处是:状态(加载中、进度、标题)都暴露给了 Compose 侧,你可以用这些状态来驱动 UI 变化,比如显示一个加载进度条。我在好几个项目里都用了这个模式,效果不错。

个人经验:封装互操作组件时,尽量把 View 的内部状态「映射」成 Compose 的 State。这样你的 Compose 代码就能用声明式的方式去响应变化,而不是到处写回调。说白了,就是把 View 的「命令式」思维,翻译成 Compose 的「声明式」语言。

好了,关于 Compose 和原生 View 的互操作,核心就是这些。记住一点:互操作是手段,不是目的。最终目标还是让代码更简洁、更可维护。如果你现在还在用纯 View 体系,不妨从一个小模块开始,试试用 ComposeView 嵌入 Compose,感受一下声明式 UI 的魅力。