虽然从 View 迁移到 Compose 仅与界面相关,但若要执行安全的增量迁移,需要考虑的因素有很多。本页介绍了将基于 View 的应用迁移到 Compose 时的一些注意事项。
迁移应用主题
推荐使用 Material Design 设计系统来为 Android 应用设置主题。
对于基于 View 的应用,可以使用三个 Material 版本:
- 使用 AppCompat 库(即
Theme.AppCompat.*
)的 Material Design 1 - 使用 MDC-Android 库(即
Theme.MaterialComponents.*
)的 Material Design 2 - 使用 MDC-Android 库(即
Theme.Material3.*
)的 Material Design 3
对于 Compose 应用,可以使用两个 Material 版本:
- 使用 Compose Material 库(即
androidx.compose.material.MaterialTheme
)的 Material Design 2 - 使用 Compose Material 3 库(即
androidx.compose.material3.MaterialTheme
)的 Material Design 3
如果应用的设计系统符合要求,建议您使用最新版本 (Material 3)。View 和 Compose 都有相应的迁移指南:
- 在 View 中从 Material 1 迁移到 Material 2
- 在 View 中从 Material 2 迁移到 Material 3
- 在 Compose 中从 Material 2 迁移到 Material 3
在 Compose 中创建新屏幕时,无论您使用的是哪个版本的 Material Design,都请确保先应用 MaterialTheme
,然后再应用任何从 Compose Material 库发出界面的可组合项。Material 组件(Button
、Text
等)依赖于现有的 MaterialTheme
,如果没有 MaterialTheme,这些组件的行为将处于未定义状态。
所有 Jetpack Compose 示例都使用基于 MaterialTheme
构建的自定义 Compose 主题。
如需了解详情,请参阅 Compose 中的设计系统和将 XML 主题迁移到 Compose。
导航
如果您在应用中使用了 Navigation 组件,请参阅使用 Compose 进行导航 - 互操作性和将 Jetpack Navigation 迁移到 Navigation Compose,了解详情。
测试混合 Compose/View 界面
将应用的某些部分迁移到 Compose 后,若要确保您没有破坏任何内容,测试至关重要。
当 activity 或 fragment 使用 Compose 时,您需要使用 createAndroidComposeRule
,而不是 ActivityScenarioRule
。createAndroidComposeRule
会将 ActivityScenarioRule
与 ComposeTestRule
集成,让您可以同时测试 Compose 和 View 代码。
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
如需详细了解测试,请参阅测试 Compose 布局。如需了解与界面测试框架的互操作性,请参阅与 Espresso 的互操作性和与 UiAutomator 的互操作性。
将 Compose 与您现有的应用架构集成
单向数据流 (UDF) 架构模式可与 Compose 无缝协作。如果应用改用其他类型的架构模式,例如 Model View Presenter (MVP),我们建议您在采用 Compose 之前或期间将界面中的相应部分迁移到 UDF。
在 Compose 中使用 ViewModel
如果您使用 Architecture Components ViewModel
库,可以通过调用 viewModel()
函数,从任意可组合项访问 ViewModel
,如 Compose 和其他库中所述。
采用 Compose 时,请务必注意在不同的可组合项中使用相同的 ViewModel
类型,因为 ViewModel
元素遵循视图生命周期作用域。如果使用 Navigation 库,作用域是主机 activity、fragment 或导航图。
例如,如果可组合项托管在 activity 中,viewModel()
始终返回相同实例,该实例只有在 activity 完成时才被清除。在以下示例中,系统会向同一用户(“user1”)显示两次问候语,因为主机 activity 中所有可组合项都重复使用了相同的 GreetingViewModel
实例。其他可组合项重复使用了创建的第一个 ViewModel
实例。
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
由于导航图还限定了 ViewModel
元素的作用域,因此作为导航图中某个目标位置的可组合项具有不同的 ViewModel
实例。在这种情况下,ViewModel
的作用域是目标位置的生命周期,它会在从返回堆栈中移除目标位置时被清除。在以下示例中,当用户转到“个人资料”屏幕时,系统会创建新的 GreetingViewModel
实例。
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
状态可信来源
当您在界面的某个部分采用 Compose 时,Compose 和 View 系统代码可能需要共享数据。如有可能,我们建议您将相应共享状态封装在另一个遵循两个平台所用的 UDF 最佳实践的类中,例如,封装在会公开共享数据流以发出数据更新的 ViewModel
中。
但是,如果要共享的数据会发生变化或与界面元素密切相关,这种方式就不可行。在这种情况下,必须有一个系统是可信来源,同时该系统需要与另一系统共享所有数据更新。一般来说,可信来源应由更靠近界面层次结构根目录的任一元素拥有。
将 Compose 视为可信来源
使用 SideEffect
可组合项向非 Compose 代码发布 Compose 状态。在这种情况下,可信来源会保留在发送状态更新的可组合项中。
例如,您的分析库可能允许您通过将自定义元数据(在此示例中为“用户属性”)附加到所有后续分析事件,来细分用户群体。如需将当前用户的用户类型传递给您的分析库,请使用 SideEffect
更新其值。
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
如需了解详情,请参阅 Compose 中的附带效应。
将 View 系统视为可信来源
如果 View 系统拥有状态并将其与 Compose 共享,我们建议您将相应状态封装在 mutableStateOf
对象中,以保证 Compose 的线程安全。如果您使用此方式,可组合函数将会有所简化,因为它们不再拥有可信来源,但 View 系统需要更新可变状态以及使用相应状态的视图。
在以下示例中,CustomViewGroup
包含 TextView
以及内含 TextField
可组合项的 ComposeView
。TextView
需要显示用户在 TextField
中输入的内容。
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
迁移共享界面
如果您要逐步迁移到 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( containerColor = MaterialTheme.colorScheme.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("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
请注意,可组合项参数在自定义视图中会成为可变变量。这会使自定义 CallToActionViewButton
视图变得可膨胀且可以使用,像传统视图一样。请参阅下面的使用视图绑定功能时的示例:
class ViewBindingActivity : ComponentActivity() { 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.greeting) onClick = { /* Do something */ } } } }
如果自定义组件包含可变状态,请参阅状态可信来源部分。
优先考虑将状态与呈现分开
过去,View
是有状态的。View
管理的字段用于描述要显示的内容以及显示方式。将 View
转换为 Compose 时,需要将正在渲染的数据隔离开以实现单向数据流,状态提升中对此进行了详细说明。
例如,View
具有 visibility
属性,用于描述该 View 是可见、不可见还是已消失。这是 View
固有的属性。虽然其他代码可能会改变 View
的可见性,但只有 View
本身知道它当前的真实可见性。用于确保 View
可见的逻辑很容易出错,并且通常与 View
本身相关联。
相比之下,Compose 通过使用 Kotlin 中的条件逻辑,可以轻松显示完全不同的可组合项:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
根据设计,CautionIcon
不需要知道或关心其显示的原因,也没有 visibility
的概念:它要么在组合中,要么不在。
通过将状态管理与内容呈现逻辑完全分开,您能够以状态转换的形式更自由地更改将内容显示到界面的方式。能够在需要时提升状态还会提高可组合项的可重用性,因为状态所有权更灵活。
提升封装组件和可重用组件
View
元素通常对自己所处位置有所感知:在 Activity
、Dialog
、Fragment
内或另一个 View
层次结构中的某个位置。由于 View
通常是从静态布局文件膨胀而来,因此其整体结构往往非常严格。这会使耦合更紧密,并且使 View
更难以更改或重复使用。
例如,自定义 View
可能假定它具有某种类型的子视图(具有特定 ID),并直接根据某项操作更改其属性。这使得这些 View
元素紧密耦合在一起:如果自定义 View
找不到子级,则可能会发生崩溃或损坏;而如果没有自定义 View
父级,子级可能会无法重复使用。
这在具有可重用可组合项的 Compose 中则不是什么问题。父级可以轻松指定状态和回调,因此您可以编写可重用可组合项,而不必知道它们具体将被用在哪里。
@Composable fun AScreen() { 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 可让您使用常规条件逻辑完全在代码中更改布局,从而更轻松地实现此目的。如需了解详情,请参阅使用窗口大小类别。
此外,如需了解 Compose 提供了哪些技术来构建自适应界面,请参阅支持不同的屏幕尺寸。
使用 View 实现嵌套滚动
如需详细了解如何在可滚动的 View 元素与可滚动的可组合项之间实现嵌套滚动互操作(相互嵌套),请仔细阅读嵌套滚动互操作性。
RecyclerView
中的 Compose
自 RecyclerView
版本 1.3.0-alpha02 发布以来,RecyclerView
中的可组合项性能一直非常出色。请确保您使用的 RecyclerView
版本不低于 1.3.0-alpha02,以便切实感受这些好处。
WindowInsets
与 View 的互操作性
如果您的屏幕在同一层次结构中同时包含 View 和 Compose 代码,您可能需要替换默认的内边距。在这种情况下,您需要明确说明哪个视图应使用内边距,哪个视图应忽略内边距。
例如,如果最外层布局是 Android View 布局,您应在 View 系统中使用内边距,并忽略 Compose 中的内边距。或者,如果最外层布局是可组合项,您应在 Compose 中使用内边距,并相应地为 AndroidView
可组合项添加内边距。
默认情况下,每个 ComposeView
都会在 WindowInsetsCompat
级别的使用情况中消耗所有边衬区。如需更改此默认行为,请将 ComposeView.consumeWindowInsets
设置为 false
。
如需了解详情,请参阅 Compose 中的 WindowInsets
文档。
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 显示表情符号
- Compose 中的 Material Design 2
- Compose 中的窗口边衬区