27、Compose与原生View互操作:AndroidView、ComposeView、Fragment集成、WebView集成
说实话,Jetpack Compose 虽然好,但现实项目里不可能一夜之间全换成 Compose。你想想看,一个做了两三年的项目,里面几十个 Fragment、上百个自定义 View,全重写?不现实。所以 Compose 和原生 View 怎么共存,就成了每个 Android 开发者必须掌握的技能。
我个人习惯把这种互操作分成两个方向:在 Compose 里用原生 View,和在原生 View 里用 Compose。今天我们就把这俩方向都捋一遍。
一、知识体系总览
先看一张图,把今天要讲的东西串起来。我每次讲这个主题都会先画这张图,学员反馈说思路一下子就清晰了。
二、在 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
}
)
}
三、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()
}
}
}
四、在原生 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("点击我")
}
}
}
}
}
}
五、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)
}
六、生命周期与状态同步
这是互操作里最容易出问题的地方。我总结了一个原则:谁创建,谁管理。
| 场景 | 生命周期管理 | 状态同步方式 |
|---|---|---|
| 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()
}
}
}
七、总结
Compose 和原生 View 的互操作,说白了就两件事:AndroidView 把原生 View 装进 Compose,ComposeView 把 Compose 内容装进原生 View。剩下的都是细节——生命周期管理、状态同步、性能优化。
我个人建议,在项目迁移时采用渐进式策略:新页面用纯 Compose,老页面用 ComposeView 逐步替换。不要想着一次性全部重写,那样风险太大。我见过一个团队试图一个月内把整个项目改成 Compose,结果上线后崩溃率飙升了三倍。
嗯,今天就到这里。记住:互操作是手段,不是目的。最终目标还是让代码更清晰、维护更简单。