如果您的应用界面是基于 View 系统,您可能不想一次全部重写整个界面。本页将帮助您向现有界面中添加新的 Compose 元素。
迁移共享界面
如果您要逐步迁移到 Compose,可能需要在 Compose 和 View 系统中都使用共享界面元素。例如,如果您的应用具有自定义 CallToActionButton
组件,您可能需要在 Compose 和基于 View 的屏幕中都使用它。
在 Compose 中,共享界面元素成为可在整个应用中重复使用的可组合项,无论元素是采用 XML 进行的样式设计还是一个自定义视图。例如,您将为自定义号召性用语 Button
组件创建 CallToActionButton
可组合项。
为了在基于 View 的屏幕中使用可组合项,您需要创建一个从 AbstractComposeView
扩展的自定义视图封装容器。在该容器被替换的 Content
可组合项中,将您创建的可组合项封装在 Compose 主题中,如下例所示:
@Composable
fun CallToActionButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = onClick,
modifier = modifier,
) {
Text(text)
}
}
class CallToActionViewButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var text by mutableStateOf<String>("")
var onClick by mutableStateOf<() -> Unit>({})
@Composable
override fun Content() {
YourAppTheme {
CallToActionButton(text, onClick)
}
}
}
请注意,可组合项参数在自定义视图中会成为可变变量。这会使自定义 CallToActionViewButton
视图在使用视图绑定等功能时变得可膨胀且可以使用,像传统视图一样。请参见下面的示例:
class ExampleActivity : Activity() {
private lateinit var binding: ActivityExampleBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityExampleBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.callToAction.apply {
text = getString(R.string.something)
onClick = { /* Do something */ }
}
}
}
如果自定义组件包含可变状态,请参阅状态可信来源部分。
主题
建议您按照 Material Design 的准则,采用适用于 Android 的 Material Design 组件(MDC) 库来设计 Android 应用的主题。如 Compose 主题文档中所述,Compose 使用 MaterialTheme
可组合项实现这些概念。
在 Compose 中创建新屏幕时,您应确保在使用任何从 Material 组件库中发出界面的可组合项之前使用 MaterialTheme
。Material 组件(Button
和 Text
等)依赖于现有的 MaterialTheme
,如果没有 MaterialTheme
,这些组件的行为将处于未定义状态。
所有 Jetpack Compose 示例都使用基于 MaterialTheme
构建的自定义 Compose 主题。
多个可信来源
现有应用可能包含大量视图主题和样式。在现有应用中引入 Compose 时,您需要迁移主题才能对任意 Compose 屏幕使用 MaterialTheme
。这意味着应用的主题将会有 2 个可信来源:基于 View 的主题,以及 Compose 主题。样式上的任何更改都需要在多处实施。
如果您计划将应用完全迁移到 Compose,最终还是要针对现有主题创建 Compose 版本。问题在于,在开发过程中创建 Compose 主题的时间越早,开发中需要进行的维护就越多。
MDC Compose 主题适配器
如果您在 Android 应用中使用 MDC 库,则可借助 MDC Compose 主题适配器库,在可组合项中轻松地重复使用基于 View 的现有主题的颜色、排版和形状主题:
import com.google.android.material.composethemeadapter.MdcTheme
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Use MdcTheme instead of MaterialTheme
// Colors, typography, and shape have been read from the
// View-based theme used in this Activity
MdcTheme {
ExampleComposable(/*...*/)
}
}
}
}
如需了解详情,请参阅 MDC 库文档。
AppCompat Compose 主题适配器
借助 AppCompat Compose 主题适配器库,您可以在 Jetpack Compose 中轻松地重复使用 AppCompat XML 主题。它会使用上下文主题的颜色和排版值创建 MaterialTheme
。
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppCompatTheme {
// Colors, typography, and shape have been read from the
// View-based theme used in this Activity
ExampleComposable(/*...*/)
}
}
}
}
默认组件样式
MDC 和 AppCompat Compose 主题适配器库不会读取任何由主题定义的默认微件样式。这是因为 Compose 没有默认可组合项的概念。
Compose 中的主题叠加层
将基于 View 的屏幕迁移到 Compose 时,请注意 android:theme
属性的用法。您可能需要在 Compose 界面树的相应部分添加新的 MaterialTheme
。
如需了解详情,请参阅主题指南。
WindowInsets 和 IME 动画
从 Compose 1.2.0 开始,您可以在布局中使用修饰符处理 WindowInsets
。IME 动画也受支持。
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MaterialTheme {
MyScreen()
}
}
}
}
@Composable
fun MyScreen() {
Box {
LazyColumn(
modifier = Modifier
.fillMaxSize() // fill the entire window
.imePadding() // padding for the bottom for the IME
.imeNestedScroll() // scroll IME at the bottom
)
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp) // normal 16dp of padding for FABs
.navigationBarsPadding() // padding for navigation bar
.imePadding() // padding for when IME appears
onClick = { }
) {
Icon( /* ... */)
}
}
}
图 2. IME 动画
如需了解详情,请参阅 accompanists-insets 库文档。
优先考虑将状态与呈现分开
过去,View
是有状态的。View
管理的字段用于描述要显示的内容以及显示方式。将 View
转换为 Compose 时,需要将正在渲染的数据隔离开以实现单向数据流,状态提升中对此进行了详细说明。
例如,View
具有 visibility
属性,用于描述该 View 是可见、不可见还是已消失。这是 View
固有的属性。虽然其他代码可能会改变 View
的可见性,但只有 View
本身知道它当前的真实可见性。用于确保 View
可见的逻辑很容易出错,并且通常与 View
本身相关联。
相比之下,Compose 通过使用 Kotlin 中的条件逻辑,可以轻松显示完全不同的可组合项:
if (showCautionIcon) {
CautionIcon(/* ... */)
}
根据设计,CautionIcon
不需要知道或关心其显示的原因,也没有 visibility
的概念:它要么在组合中,要么不在。
通过将状态管理与内容呈现逻辑完全分开,您可以更自由地更改将内容作为状态转换显示到界面的方式。能够在需要时提升状态还会提升可组合项的可重用性,因为状态所有权更灵活。
提升封装组件和可重用组件
View
元素通常对自己所处位置有所感知:在 Activity
、Dialog
、Fragment
内或另一个 View
层次结构中的某个位置。由于 View
通常是从静态布局文件膨胀而来,因此其整体结构往往非常严格。这会使耦合更紧密,并且使 View
更难以更改或重复使用。
例如,自定义 View
可能假定它具有某种类型的子视图(具有特定 ID),并直接根据某项操作更改其属性。这使得这些 View
元素紧密耦合在一起:如果自定义 View
找不到子级,则可能会发生崩溃或损坏;而如果没有自定义 View
父级,子级可能会无法重复使用。
这在具有可重用可组合项的 Compose 中则不是什么问题。父级可以轻松指定状态和回调,因此可以编写可重用可组合项,而不必知道它们具体将被用在哪里。
var isEnabled by rememberSaveable { mutableStateOf(false) }
Column {
ImageWithEnabledOverlay(isEnabled)
ControlPanelWithToggle(
isEnabled = isEnabled,
onEnabledChanged = { isEnabled = it }
)
}
在上面的示例中,所有三个部分封装程度更高,但耦合程度更低:
ImageWithEnabledOverlay
只需知道当前的isEnabled
状态,而不需知道ControlPanelWithToggle
的存在,甚至不需要知道如何控制它。ControlPanelWithToggle
不知道ImageWithEnabledOverlay
的存在。isEnabled
可能以零种、一种或多种方式显示,而ControlPanelWithToggle
无需更改。对父级而言,
ImageWithEnabledOverlay
或ControlPanelWithToggle
的嵌套深度无关紧要。这些子项的目的可能是为变化添加动画效果、换出内容或将内容传递给其他子项。
此模式称为“控制反转”,CompositionLocal
文档中对此做了更详细的介绍。
处理屏幕尺寸的变化
针对不同尺寸的窗口提供不同的资源是创建自适应 View
布局的主要方式之一。虽然在确定屏幕级别的布局时仍可选择使用限定资源,但 Compose 可让您使用常规条件逻辑完全在代码中更改布局,从而更轻松地实现此目的。使用 BoxWithConstraints
等工具,可以根据各元素可使用的空间来确定布局,使用限定资源却无法做到这一点:
@Composable
fun MyComposable() {
BoxWithConstraints {
if (minWidth < 480.dp) {
/* Show grid with 4 columns */
} else if (minWidth < 720.dp) {
/* Show grid with 8 columns */
} else {
/* Show grid with 12 columns */
}
}
}
如需了解 Compose 提供了哪些技术来构建自适应界面,请参阅构建自适应布局。
使用 View 实现嵌套滚动
遗憾的是,View 系统和 Jetpack Compose 之间的嵌套滚动尚不可用。您可以在此问题跟踪器 bug 中查看进度。
Compose 在 RecyclerView 中的运用
Jetpack Compose 将 DisposeOnDetachedFromWindow
用作默认 ViewCompositionStrategy
。这意味着,每当视图与窗口分离时,系统都会处置组合。
将 ComposeView
用作 RecyclerView
ViewHolder 的一部分时,默认策略的效率并不高,因为在 RecyclerView
与窗口分离之前,底层组合实例将一直保留在内存中。最好在 RecyclerView
不再需要 ComposeView
时处置底层组合。
借助 disposeComposition
函数,您可以手动处置 ComposeView
的底层组合。您可以在相应视图被回收时调用此函数,如下所示:
import androidx.compose.ui.platform.ComposeView
class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MyComposeViewHolder {
return MyComposeViewHolder(ComposeView(parent.context))
}
override fun onViewRecycled(holder: MyComposeViewHolder) {
// Dispose the underlying Composition of the ComposeView
// when RecyclerView has recycled this ViewHolder
holder.composeView.disposeComposition()
}
/* Other methods */
}
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
/* ... */
}
正如 Interoperability API 指南中的“ComposeView 的 ViewCompositionStrategy”部分所述,为了使 Compose ViewHolder 能够适用于所有场景,有必要使用 DisposeOnViewTreeLifecycleDestroyed
策略。
import androidx.compose.ui.platform.ViewCompositionStrategy
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bind(input: String) {
composeView.setContent {
MdcTheme {
Text(input)
}
}
}
}
如需了解 RecyclerView
中所用的 ComposeView
的实际运用,请查看 Sunflower 应用的 compose_recyclerview
分支。