Interoperability API

在应用中采用 Compose 时,Compose 可以与基于视图的界面相结合。下面列出了一些可让您更轻松地过渡到 Compose 的 API、建议和提示。

Android View 中的 Compose

您可以将基于 Compose 的界面添加到采用基于视图的设计的现有应用中。

如需创建完全基于 Compose 的新屏幕,请让 activity 调用 setContent() 方法,并传递您想要使用的任何可组合函数。

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent { // In here, we can call composables!
            MaterialTheme {
                Greeting(name = "compose")
            }
        }
    }
}

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

这段代码看起来就和只含 Compose 元素的应用中的代码一样。

ComposeView 的 ViewCompositionStrategy

默认情况下,只要视图与窗口分离,Compose 就会处理组合。Compose 界面 View 类型(例如 ComposeViewAbstractComposeView)使用定义此行为的 ViewCompositionStrategy

默认情况下,Compose 使用 DisposeOnDetachedFromWindow 策略。但是,在 Compose 界面 View 类型用于以下各项的部分情况下,您可能并不希望使用此默认值

  • Fragment。组合必须遵循 fragment 的视图生命周期,Compose 界面 View 类型才能保存状态。

  • 转换。每当在转换过程中使用 Compose 界面 View 时,系统都会在转换开始(而不是转换结束)时将其与窗口分离,从而导致您的可组合项在它仍然在屏幕上时处理其状态。

  • RecyclerView 视图持有者,或您自己的由生命周期管理的自定义 View

在上述某些情况下,除非您手动调用 AbstractComposeView.disposeComposition,否则应用还可能会缓慢从组合实例泄漏内存。

如需在不再需要组合时自动处理组合,请通过调用 setViewCompositionStrategy 方法设置其他策略或创建自己的策略。例如,DisposeOnLifecycleDestroyed 策略会在 lifecycle 被销毁时处理组合。此策略适用于与已知的 LifecycleOwner 具有一对一关系的 Compose 界面 View 类型。当 LifecycleOwner 未知时,可以使用 DisposeOnViewTreeLifecycleDestroyed

如需了解如何使用此 API,请参阅“Fragment 中的 ComposeView”部分

Fragment 中的 ComposeView

如果您要将 Compose 界面内容并入 fragment 或现有视图布局,请使用 ComposeView 并调用其 setContent() 方法。ComposeView 是一个 Android View

您可以将 ComposeView 放在 XML 布局中,就像放置其他任何 View 一样:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

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

</LinearLayout>

在 Kotlin 源代码中,通过 XML 中定义的布局资源使布局膨胀。然后,使用 XML ID 获取 ComposeView,设置最适合宿主 View 的组合策略,并调用 setContent() 以使用 Compose。

class ExampleFragment : Fragment() {

    private var _binding: FragmentExampleBinding? = null
    // This property is only valid between onCreateView and onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentExampleBinding.inflate(inflater, container, false)
        val view = binding.root
        view.composeView.apply {
            // Dispose the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

两个略有不同的文本元素,一个在另一个之上

图 1. 此图显示了在 View 界面层次结构中添加 Compose 元素的代码的输出。“Hello Android!”文本由 TextView 微件显示。“Hello Compose!”文本由 Compose 文本元素显示。

如果整个屏幕是使用 Compose 构建的,您还可以直接在 fragment 中添加 ComposeView,这样可让您完全避免使用 XML 布局文件。

class ExampleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            // Dispose the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }
}

如果同一布局中存在多个 ComposeView 元素,每个元素必须具有唯一的 ID 才能使 savedInstanceState 发挥作用。

class ExampleFragment : Fragment() {

  override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
          id = R.id.compose_view_x
          ...
      })
      addView(TextView(...))
      addView(ComposeView(...).apply {
          id = R.id.compose_view_y
          ...
      })
    }
  }
}

ComposeView ID 在 res/values/ids.xml 文件中进行定义:

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

Compose 中的 Android View

您可以在 Compose 界面中添加 Android View 层次结构。如果您要使用 Compose 中尚未提供的界面元素(如 AdViewMapView),此方法特别有用。此方法还可让您重复使用您可能已设计的自定义视图。

如需添加视图元素或层次结构,请使用 AndroidView 可组合项。系统会向 AndroidView 传递一个返回 View 的 lambda。AndroidView 还提供了在视图膨胀时被调用的 update 回调。每当在该回调中读取的 State 发生变化时,AndroidView 都会重组。

@Composable
fun CustomView() {
    val selectedItem = remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates custom view
            CustomView(context).apply {
                // Sets up listeners for View -> Compose communication
                myView.setOnClickListener {
                    selectedItem.value = 1
                }
            }
        },
        update = { view ->
            // View's been inflated or state read in this block has been updated
            // Add logic here if necessary

            // As selectedItem is read here, AndroidView will recompose
            // whenever the state changes
            // Example of Compose -> View communication
            view.coordinator.selectedItem = selectedItem.value
        }
    )
}

@Composable
fun ContentExample() {
    Column(Modifier.fillMaxSize()) {
        Text("Look at this CustomView!")
        CustomView()
    }
}

如需嵌入 XML 布局,请使用 androidx.compose.ui:ui-viewbinding 库提供的 AndroidViewBinding API。为此,您的项目必须启用视图绑定。

与许多其他内置可组合项一样,AndroidView 接受 Modifier 参数,该参数可用于设置它在父级可组合项中的位置等用途。

@Composable
fun AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

从 Compose 调用 Android 框架

Compose 与 Android 框架类密切相关。例如,Compose 托管在 Android View 类(如 ActivityFragment)上,并且可能需要利用 Android 框架类(例如 Context、系统资源、ServiceBroadcastReceiver)。

如需详细了解系统资源,请参阅 Compose 中的资源文档。

CompositionLocal

CompositionLocal 类允许通过可组合函数隐式传递数据。它们通常在界面树的某个节点具有一个值。该值可供其可组合项的后代使用,而无需在可组合函数中将 CompositionLocal 声明为参数。

CompositionLocal 用于为 Compose 中的 Android 框架类型(例如 ContextConfigurationView)传递值,其中 Compose 代码托管在相应的 LocalContextLocalConfigurationLocalView 中。请注意,CompositionLocal 类的前缀是 Local,IDE 中的自动补全功能可以很轻松地检测到这些类。

使用 CompositionLocalcurrent 属性访问前者当前值。例如,以下代码通过调用 LocalContext.current 使用 Compose 界面树相应部分中的 Context 创建自定义视图。

@Composable
fun rememberCustomView(): CustomView {
    val context = LocalContext.current
    return remember { CustomView(context).apply { /*...*/ } }
}

如需查看更完整的示例,请参阅本文档末尾的案例研究:BroadcastReceiver 部分。

其他交互

如果没有为您需要的交互定义实用程序,最佳做法是遵循常规 Compose 准则,即数据向下流动而事件向上流动(Compose 编程思想一文对此进行了更为详细的说明)。例如,以下可组合函数会启动一个不同的 activity:

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(/*...*/)
                })
            }
        }
    }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
    Button(onClick = onButtonClick) {
        Text(data.title)
    }
}

案例研究:BroadcastReceiver

举一个更实际的例子,您可能想要迁移一些功能或在 Compose 中实现一些功能,以及展示 CompositionLocal附带效应,在这种情况下,需要通过可组合函数注册 BroadcastReceiver

该解决方案利用 LocalContext 来使用当前上下文,以及展示 rememberUpdatedStateDisposableEffect 附带效应。

@Composable
fun SystemBroadcastReceiver(
    systemAction: String,
    onSystemEvent: (intent: Intent?) -> Unit
) {
    // Grab the current context in this part of the UI tree
    val context = LocalContext.current

    // Safely use the latest onSystemEvent lambda passed to the function
    val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)

    // If either context or systemAction changes, unregister and register again
    DisposableEffect(context, systemAction) {
        val intentFilter = IntentFilter(systemAction)
        val broadcast = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                onSystemEvent(intent)
            }
        }

        context.registerReceiver(broadcast, intentFilter)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            context.unregisterReceiver(broadcast)
        }
    }
}

@Composable
fun HomeScreen() {

    SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
        val isCharging = /* Get from batteryStatus ... */ true
        /* Do something if the device is charging */
    }

    /* Rest of the HomeScreen */
}