27、Compose与原生View互操作:AndroidView、ComposeView、Fragment集成、WebView集成

说实话,Jetpack Compose 虽然好,但现实项目里不可能一夜之间全换成 Compose。你想想看,一个做了两三年的项目,里面几十个 Fragment、上百个自定义 View,全重写?不现实。所以 Compose 和原生 View 怎么共存,就成了每个 Android 开发者必须掌握的技能。

我个人习惯把这种互操作分成两个方向:在 Compose 里用原生 View,和在原生 View 里用 Compose。今天我们就把这俩方向都捋一遍。

一、知识体系总览

先看一张图,把今天要讲的东西串起来。我每次讲这个主题都会先画这张图,学员反馈说思路一下子就清晰了。

Compose ↔ 原生View互操作 Compose 中使用原生 View 原生 View 中使用 Compose AndroidView WebView 集成 MapView 等复杂 View 自定义原生 View ComposeView Fragment 集成 XML 布局中嵌入 RecyclerView 中混用 核心原则:生命周期管理 + 状态同步

二、在 Compose 中嵌入原生 View:AndroidView

AndroidView 是 Compose 提供的一个「桥接组件」。说白了,它就是一个 Compose 组件,但里面装的是原生 View。我在项目里最常用的场景就是嵌入 WebView 和 MapView,因为这两个东西在 Compose 里没有原生替代品。

2.1 基本用法

@Composable
fun MyAndroidView() {
    AndroidView(
        factory = { context ->
            // 在这里创建原生 View
            TextView(context).apply {
                text = "我是原生 TextView"
                textSize = 18f
                setTextColor(Color.BLUE)
            }
        },
        modifier = Modifier.fillMaxWidth().height(100.dp)
    )
}

嗯,这里要注意:factory 只在首次组合时调用一次。如果你需要根据状态更新 View,要用 update 参数。

2.2 带状态更新的 AndroidView

我曾经踩过一个坑:在 factory 里直接读取了某个 MutableState,结果状态变了 View 不刷新。后来才发现,状态更新逻辑要放在 update 里。

@Composable
fun DynamicAndroidView(text: String) {
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                textSize = 16f
            }
        },
        update = { textView ->
            // 状态变化时更新
            textView.text = text
        }
    )
}
💡 我的经验: factory 里只做创建和初始化,所有跟状态绑定的逻辑都放 update 里。这样 Compose 重组时才会正确更新原生 View。

三、WebView 集成实战

WebView 是 AndroidView 最典型的应用场景。我在做混合开发时,经常需要在 Compose 页面里嵌入 H5 页面。这里有个关键点:WebView 的生命周期需要手动管理

@Composable
fun ComposeWebView(url: String) {
    var webView by remember { mutableStateOf<WebView?>(null) }

    AndroidView(
        factory = { context ->
            WebView(context).apply {
                settings.javaScriptEnabled = true
                settings.domStorageEnabled = true
                webViewClient = WebViewClient()
                webChromeClient = WebChromeClient()
                loadUrl(url)
                webView = this
            }
        },
        modifier = Modifier.fillMaxSize()
    )

    DisposableEffect(Unit) {
        onDispose {
            webView?.destroy()
        }
    }
}
⚠️ 避坑指南: 我曾经忘记在 onDispose 里调用 webView.destroy(),结果导致内存泄漏。WebView 会持有 Activity 的引用,不销毁的话,整个 Activity 都 leak 了。

四、在原生 View 中使用 Compose:ComposeView

反过来,如果你有一个老项目,想在某个 Fragment 或 XML 布局里嵌入 Compose 内容,就用 ComposeView。它本质上是一个原生 View,但里面跑的是 Compose 代码。

4.1 在 XML 中嵌入

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是原生 TextView" />

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

</LinearLayout>
// 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 {
            MaterialTheme {
                Text(
                    text = "我是 Compose 内容",
                    fontSize = 20.sp,
                    color = Color.Red
                )
            }
        }
    }
}

4.2 在 Fragment 中集成

Fragment 集成 Compose 是我在项目迁移时最常用的方式。我的策略是:新功能用 Compose 写,老功能保持原生,通过 Fragment 容器做隔离

class MyComposeFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
            )
            setContent {
                // 这里写 Compose 代码
                Column(modifier = Modifier.padding(16.dp)) {
                    Text("Fragment 中的 Compose")
                    Button(onClick = { /* 处理点击 */ }) {
                        Text("点击我")
                    }
                }
            }
        }
    }
}
🔑 关键点: setViewCompositionStrategy 决定了 Compose 内容何时销毁。我个人推荐 DisposeOnViewTreeLifecycleDestroyed,它会跟随 Fragment 的生命周期自动清理,省心。

五、RecyclerView 中混用 Compose

这个场景比较特殊。你想想看,一个 RecyclerView 里,有些 item 是原生 View,有些 item 是 Compose 内容。怎么做?

其实思路很简单:在 Adapter 的 onCreateViewHolder 里,根据 itemType 返回不同的 ViewHolder。如果 item 需要 Compose,就返回一个包含 ComposeView 的 ViewHolder。

class MixedAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    companion object {
        private const val TYPE_NATIVE = 0
        private const val TYPE_COMPOSE = 1
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TYPE_NATIVE -> {
                val view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.item_native, parent, false)
                NativeViewHolder(view)
            }
            TYPE_COMPOSE -> {
                val composeView = ComposeView(parent.context).apply {
                    layoutParams = ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                }
                ComposeViewHolder(composeView)
            }
            else -> throw IllegalArgumentException("Unknown view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ComposeViewHolder -> {
                holder.composeView.setContent {
                    // 这里写 Compose 内容
                    Text("Compose Item at position $position")
                }
            }
            is NativeViewHolder -> {
                // 绑定原生 View 数据
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        // 根据 position 返回类型
        return if (position % 2 == 0) TYPE_NATIVE else TYPE_COMPOSE
    }

    override fun getItemCount() = 20

    class NativeViewHolder(view: View) : RecyclerView.ViewHolder(view)
    class ComposeViewHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView)
}
💡 性能提示: RecyclerView 中频繁调用 setContent 会有性能开销。如果 Compose item 内容不变,可以考虑用 remember 缓存 Compose 内容。我在一个列表页里试过,优化后滑动帧率从 45fps 提升到了 58fps。

六、生命周期与状态同步

这是互操作里最容易出问题的地方。我总结了一个原则:谁创建,谁管理

场景 生命周期管理 状态同步方式
Compose 中的 AndroidView 由 Compose 组合管理,DisposableEffect 清理 通过 update 参数同步 Compose 状态
原生中的 ComposeView 由原生 View 树管理,ViewCompositionStrategy 控制 通过回调或 StateFlow 同步到 Compose
Fragment 中的 Compose 跟随 Fragment 生命周期 通过 ViewModel 共享状态

举个例子,我在一个项目里需要在 Compose 页面中嵌入一个原生地图 MapView。MapView 有自己的生命周期(onCreate、onResume、onPause、onDestroy),这些生命周期必须手动调用。

@Composable
fun MapViewComposable() {
    val context = LocalContext.current
    var mapView by remember { mutableStateOf<MapView?>(null) }

    AndroidView(
        factory = { ctx ->
            MapView(ctx).apply {
                // 手动调用生命周期方法
                onCreate(Bundle())
                onResume()
                mapView = this
            }
        },
        modifier = Modifier.fillMaxSize()
    )

    DisposableEffect(Unit) {
        onDispose {
            mapView?.onPause()
            mapView?.onDestroy()
        }
    }
}
⚠️ 重要提醒: 有些第三方库的 View(如地图、视频播放器)有严格的生命周期要求。不按顺序调用生命周期方法,轻则功能异常,重则直接崩溃。我见过有人没调 onPause 就调 onDestroy,结果地图 SDK 报 NullPointerException。

七、总结

Compose 和原生 View 的互操作,说白了就两件事:AndroidView 把原生 View 装进 ComposeComposeView 把 Compose 内容装进原生 View。剩下的都是细节——生命周期管理、状态同步、性能优化。

我个人建议,在项目迁移时采用渐进式策略:新页面用纯 Compose,老页面用 ComposeView 逐步替换。不要想着一次性全部重写,那样风险太大。我见过一个团队试图一个月内把整个项目改成 Compose,结果上线后崩溃率飙升了三倍。

嗯,今天就到这里。记住:互操作是手段,不是目的。最终目标还是让代码更清晰、维护更简单。


公众号:蓝海资料掘金营,微信deep3321