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
附加到一个 ViewTreeLifecycleOwner
。ViewTreeLifecycleOwner
允许反复附加和分离视图,同时让组成保持不变。ComponentActivity
、FragmentActivity
和 AppCompatActivity
都是实现 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 中尚未提供的界面元素(如 AdView
或 MapView
),此方法特别有用。此方法还可让您重复使用您可能已设计的自定义视图。
如需添加视图元素或层次结构,请使用 AndroidView
可组合项。系统会向 AndroidView
传递一个返回 View
的 lambda。AndroidView
还提供了在视图膨胀时被调用的 update
回调。每当在该回调中读取的 State
发生变化时,AndroidView
都会重组。
@Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
val context = AmbientContext.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.value
}
}
@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(
imageVector = vectorResource(R.drawable.ic_plane),
tint = colorResource(R.color.Blue700)
)
背景信息
ContextAmbient
.current
属性可为您提供当前上下文。例如,以下代码会在当前上下文中创建一个视图:
@Composable
fun rememberCustomView(): CustomView {
val context = AmbientContext.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 解决方案。其中每个扩展程序都由不同的工件提供:
LiveData.observeAsState()
Flow.collectAsState()
Observable.subscribeAsState()
这些工件注册为监听器,并将值表示为 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.value?.let {
ShowData(dataExample)
}
}
Compose 中的异步操作
Compose 提供了一些机制,可让您从可组合项中执行异步操作。
对于基于回调的 API,您可以结合使用 MutableState
和 onCommit()
。使用 MutableState
存储回调的结果,并在结果发生变化时重组受影响的界面。每当参数发生变化时,都使用 onCommit()
来执行操作。如果界面的组成在操作完成之前结束,您也可以定义 onDispose()
方法以清除所有待处理的操作。以下示例展示了这些 API 如何协同工作。
@Composable
fun fetchImage(url: String): ImageBitmap? {
// Holds our current image, and will be updated by the onCommit lambda below
var image by remember(url) { mutableStateOf<ImageBitmap?>(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.asImageBitmap()
}
}
// 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): ImageBitmap = TODO()
@Composable
fun fetchImage(url: String): ImageBitmap? {
// This holds our current image, and will be updated by the
// launchInComposition lambda below
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
// LaunchedEffect 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.
LaunchedEffect(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(val name: String, val country: String)
@Composable
fun MyExample() {
var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}
如果某种原因导致 @Parcelize
不合适,您可以使用 mapSaver
定义自己的规则,规定如何将对象转换为系统可保存到 Bundle
的一组值。
data class City(val name: String, val 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(val name: String, val 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。