Google 致力于为黑人社区推动种族平等。查看具体举措

Compose 互操作性

Jetpack Compose 经过精心设计,可与基于视图的既定界面方法配合使用。如果您要构建新应用,最好的选择可能是使用 Compose 实现整个界面。但是,如果您要修改现有应用,您可能不希望迁移整个应用,而是可以将 Compose 与现有界面设计相结合。

您可以通过两种主要方法将 Compose 与基于视图的界面相结合:

  • 您可以将 Compose 元素添加到现有界面中,具体方法是创建完全基于 Compose 的新屏幕,或者将 Compose 元素添加到现有 Fragment 或视图布局中。
  • 您可以将基于视图的界面元素添加到可组合函数中。这样做可让您将非 Compose 微件添加到基于 Compose 的设计中。

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 元素的应用中找到的一样。

如果您要将 Compose 界面内容并入 Fragment 或现有视图布局,请使用 ComposeView 并调用其 setContent() 方法。ComposeView 是一个 Android View。您必须将 ComposeView 附加到一个 ViewTreeLifecycleOwnerViewTreeLifecycleOwner 允许反复附加和分离视图,同时让组成保持不变。ComponentActivityFragmentActivityAppCompatActivity 都是实现 ViewTreeLifecycleOwner 的类的示例。

您可以将 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,并调用 setContent() 以使用 Compose。

class ExampleFragment : Fragment() {

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    // Inflate the layout for this fragment
    return inflater.inflate(
      R.layout.fragment_example, container, false
    ).apply {
      findViewById<ComposeView>(R.id.compose_view).setContent {
        // In Compose world
        MaterialTheme {
          Text("Hello Compose!")
        }
      }
    }
  }
}

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

图 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 {
      setContent {
        MaterialTheme {
          // In Compose world
          Text("Hello Compose!")
        }
      }
    }
  }
}

如果同一布局中存在多个 ComposeView 元素,每个元素必须具有唯一的 ID 才能使 savedInstanceState 发挥作用。如需了解详情,请参阅 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) }

  val context = ContextAmbient.current
  val customView = remember {
    // Creates custom view
    CustomView(context).apply {
      // Sets up listeners for View -> Compose communication
      myView.setOnClickListener {
        selectedItem.value = 1
      }
    }
  }

  // Adds view to Compose
  AndroidView({ customView }) { view ->
    // View's been inflated - 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
  }
}

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

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

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

从 Compose 调用 View 系统

Compose 框架提供了许多 API,可让 Compose 代码与基于视图的界面交互。

系统资源

Compose 框架提供了 ...Resource() 辅助方法,可让 Compose 代码从基于视图的界面层次结构获取资源。下面是一些示例:

Text(
  text = stringResource(R.string.ok),
  modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)

Icon(
  assert = vectorResource(R.drawable.ic_plane),
  tint = colorResource(R.color.Blue700)
)

上下文

ContextAmbient.current 属性可为您提供当前上下文。例如,以下代码会在当前上下文中创建一个视图:

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

其他交互

如果没有为您需要的交互定义实用程序,最佳做法是遵循常规 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)
  }
}

与通用库集成

您可以在 Compose 中使用自己喜欢的库。本部分介绍了如何纳入一些最有用的库。

ViewModel

如果您使用架构组件 ViewModel 库,可以通过调用 viewModel() 函数,从任何可组合项访问 ViewModel

class ExampleViewModel() : ViewModel() { ... }

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()

  ... // use viewModel here
}

viewModel() 会返回一个现有的 ViewModel,或在给定范围内创建一个新的 ViewModel。只要范围处于有效状态,就会保留 ViewModel。例如,如果在某个 Activity 中使用了可组合项,则在该 Activity 完成或进程终止之前,viewModel() 会返回同一实例。

@Composable
fun MyExample() {
  // Returns the same instance as long as the activity is alive,
  // just as if you grabbed the instance from an Activity or Fragment
  val viewModel: ExampleViewModel = viewModel()
}

@Composable
fun MyExample2() {
  val viewModel: ExampleViewModel = viewModel() // Same instance as in MyExample
}

如果 ViewModel 具有依赖项,则 viewModel() 会将可选的 ViewModelProvider.Factory 作为参数。

数据流

Compose 随附了一些扩展程序,它们适用于最热门的基于流的 Android 解决方案。其中每个扩展程序都由不同的工件提供:

这些工件注册为监听器,并将值表示为 State。每当发出一个新值时,Compose 都会重组界面中使用该 state.value 的部分。例如,在以下代码中,每当 exampleLiveData 发出一个新值时,ShowData 都会重组。

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()
  val dataExample = viewModel.exampleLiveData.observeAsState()

  // Because the state is read here,
  // MyExample recomposes whenever dataExample changes.
  dataExample?.let {
    ShowData(dataExample)
  }
}

Compose 中的异步操作

Compose 提供了一些机制,可让您从可组合项中执行异步操作。

对于基于回调的 API,您可以结合使用 MutableStateonCommit()。使用 MutableState 存储回调的结果,并在结果发生变化时重组受影响的界面。每当参数发生变化时,都使用 onCommit() 来执行操作。如果界面的组成在操作完成之前结束,您也可以定义 onDispose() 方法以清除所有待处理的操作。以下示例展示了这些 API 如何协同工作。

@Composable
fun fetchImage(url: String): ImageAsset? {
    // Holds our current image, and will be updated by the onCommit lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    onCommit(url) {
        // This onCommit lambda will be invoked every time url changes

        val listener = object : ExampleImageLoader.Listener() {
            override fun onSuccess(bitmap: Bitmap) {
                // When the image successfully loads, update our image state
                image = bitmap.asImageAsset()
            }
        }

        // Now execute the image loader
        val imageLoader = ExampleImageLoader.get()
        imageLoader.load(url).into(listener)

        onDispose {
            // If we leave composition, cancel any pending requests
            imageLoader.cancel(listener)
        }
    }

    // Return the state-backed image property. Any callers of this function
    // will be recomposed once the image finishes loading
    return image
}

如果异步操作是挂起函数,您可以改用 launchInComposition()

/** Example suspending loadImage function */
suspend fun loadImage(url: String): Bitmap

@Composable
fun fetchImage(url: String): ImageAsset? {
    // This holds our current image, and will be updated by the
    // launchInComposition lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    // launchInComposition will automatically launch a coroutine to execute
    // the given block. If the `url` changes, any previously launched coroutine
    // will be cancelled, and a new coroutine launched.
    launchInComposition(url) {
        image = loadImage(url)
    }

    // Return the state-backed image property
    return image
}

SavedInstanceState

在重新创建 Activity 或进程后,您可以使用 savedInstanceState 恢复界面状态。savedInstanceState 可以在重组后保持状态。此外,savedInstanceState 也可以在重新创建 Activity 和进程后保持状态。

@Composable
fun MyExample() {
  var selectedId by savedInstanceState<String?> { null }
  ...
}

添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有几种选择。

最简单的解决方案是向对象添加 @Parcelize 注释。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。

@Parcelize
data class City(name: String, country: String): Parcelable

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}

如果某种原因导致 @Parcelize 不合适,您可以使用 mapSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值。

data class City(name: String, country: String)

val CitySaver = run {
  val nameKey = "Name"
  val countryKey = "Country"
  mapSaver(
    save = { mapOf(nameKey to it.name, nameKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
  )
}

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
}

为了避免需要为映射定义键,您也可以使用 listSaver 并将其索引用作键:

data class City(name: String, country: String)

val CitySaver = listSaver<City, Any>(
  save = { listOf(it.name, it.country) },
  restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
  ...
}

主题

如果您在应用中使用的是适用于 Android 的 Material Design 组件,您可以借助 MDC Compose 主题背景适配器库,在可组合项中轻松地重复使用现有主题背景的颜色排版形状主题:

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

    setContent {
      // We use MdcTheme instead of MaterialTheme {}
      MdcTheme {
        ExampleComposable(...)
      }
    }
  }
}

测试

您可以使用 createAndroidComposeRule() API 同时测试 View 和 Compose 组合代码。如需了解详情,请参阅测试 Compose 布局

了解详情

如需详细了解如何将 Jetpack Compose 与现有界面集成,请参阅迁移到 Jetpack Compose Codelab