分步介绍如何将基于 View 的应用实际迁移到 Jetpack Compose,以了解如何逐步采用 Compose,并探索其对架构和测试的影响。
1. 简介
Compose 和 View 系统可以结合使用。
学习内容
在此 Codelab 中,您将学习:
- 可以遵循的不同迁移路径
- 如何逐步将应用迁移到 Compose
- 如何将 Compose 添加到使用 View 构建的现有界面
- 如何在 Compose 中使用 View
- 如何在 Compose 中创建主题
- 如何测试使用 View 和 Compose 编写的混合界面
2. 迁移策略
Jetpack Compose 从设计之初就考虑到了 View 互操作性。如需迁移到 Compose,我们建议您执行增量迁移(Compose 和 View 在代码库中共存),直到应用完全迁移至 Compose 为止。
推荐的迁移策略如下:
- 使用 Compose 构建新界面
- 在构建功能时,确定可重复使用的元素,并开始创建常见界面组件库
- 一次替换一个界面的现有功能
使用 Compose 构建新界面
使用 Compose 构建覆盖整个界面的新功能是提高 Compose 采用率的最佳方式。借助此策略,您可以添加功能并利用 Compose 的优势,同时仍满足公司的业务需求
一项新功能可能涵盖整个界面,在这种情况下,整个界面都在 Compose 中。如果您使用的是基于 fragment 的导航,这意味着您需要创建一个新的 fragment,并在 Compose 中添加其内容。
您还可以在现有界面中引入新功能。在这种情况下,View 和 Compose 将共存在同一个界面上。例如,假设您要添加的功能是 RecyclerView 中的一种新的视图类型。在这种情况下,新的视图类型将位于 Compose 中,而其他项目保持不变。
构建常见界面组件库
使用 Compose 构建功能时,您很快就会意识到,您最终会构建组件库。您需要确定可重复使用的组件,促使在应用中重复使用这些组件,以便共享组件具有单一可信来源。您构建的功能随后可以依赖于这个库。
使用 Compose 替换现有功能
除了构建新功能之外,您还需要逐步将应用中的现有功能迁移到 Compose。具体采用哪种方法由您决定,下面是一些适合的方法:
- 简单界面 - 包含少数界面元素和动态元素(例如欢迎界面、确认界面或设置界面)的简单界面。这些界面非常适合迁移到 Compose,因为只需几行代码就能搞定。
- 混合 View 和 Compose 界面 - 已包含少量 Compose 代码的界面是另一个不错的选择,因为您可以继续逐步迁移该界面中的元素。如果您的某个界面在 Compose 中只有一个子树,您可以继续迁移该树的其他部分,直到整个界面位于 Compose 中。这称为自下而上的迁移方法。

3. 准备工作
获取代码
从 GitHub 获取 Codelab 代码:
shell
$ git clone https://github.com/android/codelab-android-compose
或者,您也可以下载 ZIP 文件形式的仓库:
运行示例应用
您刚刚下载的代码包含提供的所有 Compose Codelab 的代码。为了完成此 Codelab,请在 Android Studio 中打开 MigrationCodelab
项目。
在此 Codelab 中,您需要将 Sunflower 的植物详情界面迁移到 Compose。点按植物列表界面中显示的某个植物,即可打开植物详情界面。
注意 :Sunflower 的
main
分支有部分应用内容已在 Compose 中。在本练习中,我们将引用views
分支,即应用的原始实现。

项目设置
此项目使用了多个 Git 分支进行构建:
main
分支是此 Codelab 的起点。end
包含此 Codelab 的解决方案。
建议您从 main
分支中的代码着手,按照自己的节奏逐步完成此 Codelab。
在本 Codelab 中,系统会为您显示需要添加到项目的代码段。在某些地方,您还需要移除在代码段的注释中明确提及的代码。
如需使用 git 获取 end
分支,请使用 cd
指令进入 MigrationCodelab
项目的目录中,然后使用以下命令:
ruby
$ git checkout end
或从此处下载解决方案代码:
4. Sunflower 中的 Compose
Compose 已添加到您从 main
分支下载的代码中。不过,我们先来了解一下运行这些代码需要具备哪些条件。
打开应用级 build.gradle
文件后,查看该文件如何导入 Compose 依赖项,以及如何使用 buildFeatures { compose true }
标志让 Android Studio 能够运行 Compose。
app/build.gradle
gradle
android {
...
buildFeatures {
...
compose true
}
}
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2025.08.00')
implementation(composeBom)
androidTestImplementation(composeBom)
...
// Compose
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling-preview"
debugImplementation "androidx.compose.ui:ui-tooling"
...
}
这些依赖项的版本在项目级 build.gradle
文件中定义。
5. 欢迎使用 Compose!
在植物详情界面中,我们需要将对植物的说明迁移到 Compose,同时让界面的总体结构保持完好。
Compose 需要有宿主 activity 或 fragment 才能呈现界面。在 Sunflower 中,所有界面都使用 fragment,因此您需要使用 ComposeView
:这一 Android View 可以使用其 setContent
方法托管 Compose 界面内容。
移除 XML 代码
fragment_plant_detail.xml
xml
<androidx.core.widget.NestedScrollView
android:id="@+id/plant_detail_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_bottom_padding"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- Step 2) Comment out ConstraintLayout and its children --->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
<TextView
android:id="@+id/plant_detail_name"
...
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- End Step 2) Comment out until here --->
<!-- Step 3) Add a ComposeView to host Compose code --->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
添加 Compose 代码
PlantDetailDescription.kt
kotlin
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
我们从在上一步中添加的 ComposeView
中调用此可组合项,即可在界面上显示此内容。打开 PlantDetailFragment.kt
。
界面使用的是数据绑定,因此您可以直接访问 composeView
并调用 setContent
,以便在界面上显示 Compose 代码。您需要在 MaterialTheme
内调用 PlantDetailDescription
可组合项,因为 Sunflower 使用的是 Material Design。
PlantDetailFragment.kt
kotlin
class PlantDetailFragment : Fragment() {
// ...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
// ...
composeView.setContent {
// You're in Compose world!
MaterialTheme {
PlantDetailDescription()
}
}
}
// ...
}
}
注意 :Sunflower 使用 Material Design 设计颜色、排版和形状。如需对可组合项应用 Material 主题设置,您需要使用提供默认值的
MaterialTheme
可组合项。不过,如果您愿意,也可以使用自己的设计体系。如需了解更多信息,请参阅 Compose 中的设计系统。

6. 使用 XML 创建可组合项
我们首先迁移植物的名称。更确切地说,就是您在 fragment_plant_detail.xml
中移除的 ID 为 @+id/plant_detail_name
的 TextView
。
xml
<TextView
android:id="@+id/plant_detail_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@{viewModel.plant.name}"
android:textAppearance="?attr/textAppearanceHeadline5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Apple" />
请查看它是否为 textAppearanceHeadline5
样式,水平外边距为 8.dp
,以及是否在界面上水平居中。不过,要显示的标题是从由代码库层的 PlantDetailViewModel
公开的 LiveData
中观察到的。
如何观察 LiveData
将在稍后介绍,因此先假设我们有可用的名称,并以参数形式将其传递到我们在 PlantDetailDescription.kt
文件中创建的新 PlantName
可组合项。稍后,将从 PlantDetailDescription
可组合项调用此可组合项。
kotlin
@Composable
private fun PlantName(name: String) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
@Preview
@Composable
private fun PlantNamePreview() {
MaterialTheme {
PlantName("Apple")
}
}

注意 :为了避免每次想要查看代码更改时都需要将更改部署到模拟器,您可以使用 Android Studio 可组合项预览功能。
其中:
Text
的样式为MaterialTheme.typography.headlineSmall
,类似于 XML 代码中的textAppearanceHeadline5
。- 修饰符会修饰 Text,使其看起来像 XML 版本:
- 使用
fillMaxWidth
修饰符,使其占据最大可用宽度。此修饰符对应于 XML 代码中layout_width
属性的match_parent
值。 - 使用
padding
修饰符,以便应用水平内边距值margin_small
。这对应于 XML 中的marginStart
和marginEnd
声明。margin_small
值也是使用dimensionResource
辅助函数提取的现有尺寸资源。 wrapContentWidth
修饰符用于对齐文本,以使其水平居中。这类似于在 XML 中gravity
为center_horizontal
。
注意 :Compose 提供了从
dimens.xml
和strings.xml
文件获取值的简单方法,即dimensionResource(id)
和stringResource(id)
。由此一来,您可以将 View 系统视为可信来源。
7. ViewModel 和 LiveData
现在,我们将标题连接到界面。如需执行此操作,您需要使用 PlantDetailViewModel
加载数据。为此,Compose 集成了 ViewModel 和 LiveData。
ViewModels
由于在 fragment 中使用了 PlantDetailViewModel
的实例,因此我们可以将其作为参数传递给 PlantDetailDescription
,就这么简单。
注意 :在正式版应用中,
ViewModel
只能由界面级可组合项引用。如果子可组合项需要来自ViewModel
的数据,最佳实践是仅传递子可组合项所需的数据,而不是整个 ViewModel。如需了解详情,请参阅屏幕界面状态。可组合项没有自己的 ViewModel 实例,相应的实例将在可组合项和托管 Compose 代码的生命周期所有者(activity 或 fragment)之间共享。
打开 PlantDetailDescription.kt
文件,然后将 PlantDetailViewModel
参数添加到 PlantDetailDescription
:
PlantDetailDescription.kt
kotlin
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
//...
}
现在,请在从 fragment 调用此可组合项时传递 ViewModel 实例:
PlantDetailFragment.kt
kotlin
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
...
composeView.setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
LiveData
有了 LiveData,您已有权访问 PlantDetailViewModel
的 LiveData<Plant>
字段,以获取植物的名称。
如需从可组合项观察 LiveData,请使用 LiveData.observeAsState()
函数。
注意 :
LiveData.observeAsState()
开始观察 LiveData,并以State
对象表示它的值。每次向 LiveData 发布一个新值时,返回的State
都会更新,这会导致所有State.value
用例重组。
由于 LiveData 发出的值可以是 null
,因此您需要将其用例封装在 null
检查中。有鉴于此,以及为了实现可重用性,最好将 LiveData 的使用和监听拆分到不同的可组合项中。因此,我们来创建一个名为 PlantDetailContent
的新可组合项,用于显示 Plant
信息。
完成这些更新后,PlantDetailDescription.kt
文件现在应如下所示:
PlantDetailDescription.kt
kotlin
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
// Observes values coming from the VM's LiveData<Plant> field
val plant by plantDetailViewModel.plant.observeAsState()
// If plant is not null, display the content
plant?.let {
PlantDetailContent(it)
}
}
@Composable
fun PlantDetailContent(plant: Plant) {
PlantName(plant.name)
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
PlantNamePreview
应反映我们的更改,而无需直接更新,因为 PlantDetailContent
仅调用 PlantName
:

现在,您已连接 ViewModel,使植物名称能在 Compose 中显示。在接下来的几部分中,您将构建其余可组合项,并以类似的方式将它们连接到 ViewModel。
8. 更多 XML 代码迁移
现在,我们可以更轻松地将界面中缺少的内容补充完整:浇水信息和植物说明。
与您之前的操作类似,请创建一个名为 PlantWatering
的新可组合项并添加 Text
可组合项,以在界面上显示浇水信息:
PlantDetailDescription.kt
kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
Column(Modifier.fillMaxWidth()) {
// Same modifier used by both Texts
val centerWithPaddingModifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.align(Alignment.CenterHorizontally)
val normalPadding = dimensionResource(R.dimen.margin_normal)
Text(
text = stringResource(R.string.watering_needs_prefix),
color = MaterialTheme.colorScheme.primaryContainer,
fontWeight = FontWeight.Bold,
modifier = centerWithPaddingModifier.padding(top = normalPadding)
)
val wateringIntervalText = pluralStringResource(
R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
)
Text(
text = wateringIntervalText,
modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
)
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MaterialTheme {
PlantWatering(7)
}
}

需要注意以下几点:
- 由于
Text
可组合项会共享水平内边距和对齐修饰,因此您可以将修饰符分配给局部变量(即centerWithPaddingModifier
),以重复使用修饰符。修饰符是标准的 Kotlin 对象,因此可以重复使用。 - Compose 的
MaterialTheme
与plant_watering_header
中使用的colorAccent
不完全匹配。现在,我们可以使用将在互操作性主题设置部分中加以改进的MaterialTheme.colorScheme.primaryContainer
。 - 在 Compose 1.2.1 中,必须选择启用
ExperimentalComposeUiApi
才能使用pluralStringResource
。在将来的 Compose 版本中,可能不再需要这样做。
我们将各个部分组合在一起,然后同样从 PlantDetailContent
调用 PlantWatering
。
请在 PlantDetailContent
中创建一个 Column
以同时显示名称和浇水信息,并将其作为内边距。另外,为了确保背景颜色和所用的文本颜色均合适,请添加 Surface
用于处理这种设置。
PlantDetailDescription.kt
kotlin
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
}
}
}

9. Compose 代码中的 View
现在,我们来迁移植物说明。fragment_plant_detail.xml
中的代码具有包含 app:renderHtml="@{viewModel.plant.description}"
的 TextView
,用于告知 XML 在界面上显示哪些文本。renderHtml
是一个绑定适配器,可在 PlantDetailBindingAdapters.kt
文件中找到。该实现使用 HtmlCompat.fromHtml
在 TextView
上设置文本!
kotlin
@BindingAdapter("renderHtml")
fun bindRenderHtml(view: TextView, description: String?) {
if (description != null) {
view.text = HtmlCompat.fromHtml(description, FROM_HTML_MODE_COMPACT)
view.movementMethod = LinkMovementMethod.getInstance()
} else {
view.text = ""
}
}
但是,Compose 目前不支持 Spanned
类,也不支持显示 HTML 格式的文本。因此,我们需要在 Compose 代码中使用 View 系统中的 TextView
来绕过此限制。
由于 Compose 目前还无法呈现 HTML 代码,因此您需要使用 AndroidView
API 程序化地创建一个 TextView
,从而实现此目的。
AndroidView
使您能够在 View 的 factory
lamba 中构建该 View
。它还提供了一个 update
lambda,它会在 View 膨胀和后续重组时被调用。
注意 :
AndroidView
使您能够程序化地创建 View。如果您想膨胀 XML 文件中的 View,可以结合使用视图绑定与androidx.compose.ui:ui-viewbinding
库中的AndroidViewBinding
API。
为此,请创建新的 PlantDescription
可组合项。此可组合项将调用 AndroidView
,后者会在 factory
lambda 中构造 TextView
。在 factory
lambda 中,初始化显示 HTML 格式文本的 TextView
,然后将 movementMethod
设置为 LinkMovementMethod
的实例。最后,在 update
lambda 中将 TextView
的文本设置为 htmlDescription
。
PlantDetailDescription.kt
kotlin
@Composable
private fun PlantDescription(description: String) {
// Remembers the HTML formatted description. Re-executes on a new description
val htmlDescription = remember(description) {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
// Displays the TextView on the screen and updates with the HTML description when inflated
// Updates to htmlDescription will make AndroidView recompose and update the text
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
}
},
update = {
it.text = htmlDescription
}
)
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
MaterialTheme {
PlantDescription("HTML<br><br>description")
}
}

请注意,htmlDescription
会记住作为参数传递的指定 description
的 HTML 说明。如果 description
参数发生变化,系统会再次执行 remember
中的 htmlDescription
代码。
因此,如果 htmlDescription
发生变化,AndroidView
更新回调将重组。在 update
lambda 中读取的任何状态都会导致重组。
我们将 PlantDescription
添加到 PlantDetailContent
可组合项,并更改预览代码,以便同样显示 HTML 说明:
PlantDetailDescription.kt
kotlin
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
PlantDescription(plant.description)
}
}
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}

现在,您已将原始 ConstraintLayout
中的所有内容迁移到 Compose。您可以运行该应用,检查其是否按预期运行。
10. ViewCompositionStrategy
只要 ComposeView
与窗口分离,Compose 就会处理组合。如果 fragment 中使用了 ComposeView
,这种情况是不可取的,原因有两个:
- 组合必须遵循 fragment 的视图生命周期,Compose 界面
View
类型才能保存状态。 - 发生过渡时,底层
ComposeView
将处于分离状态。不过,在这些过渡期间,Compose 界面元素仍然可见。
如需修改此行为,请使用适当的 ViewCompositionStrategy
调用 setViewCompositionStrategy
,使其改为遵循 fragment 的视图生命周期。具体而言,您需要在 fragment 的 LifecycleOwner
被销毁时使用 DisposeOnViewTreeLifecycleDestroyed
策略处置组合。
由于 PlantDetailFragment
包含进入和退出过渡(如需了解详情,请查看 nav_garden.xml
),并且我们稍后会在 Compose 中使用 View
类型,因此我们需要确保 ComposeView
使用 DisposeOnViewTreeLifecycleDestroyed
策略。不过,在 fragment 中使用 ComposeView
时,最好始终设置此策略。
PlantDetailFragment.kt
kotlin
import androidx.compose.ui.platform.ViewCompositionStrategy
...
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
...
composeView.apply {
// Dispose the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
...
}
}
11. Material 主题设置
如需使用正确的主题颜色,您需要通过定义自己的主题并提供主题的颜色来自定义 MaterialTheme
。
自定义 MaterialTheme
如需创建自己的主题,请打开 theme
软件包下的 Theme.kt
文件。Theme.kt
定义了一个名为 SunflowerTheme
的可组合项,它接受内容 lambda 并将其传递给 MaterialTheme
。
它尚不会执行任何有趣的操作,接下来,您可以对其进行自定义。
Theme.kt
kotlin
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun SunflowerTheme(
content: @Composable () -> Unit
) {
MaterialTheme(content = content)
}
MaterialTheme
允许您自定义其颜色、排版和形状。现在,请通过在 Sunflower View 的主题中提供相同的颜色来自定义颜色。SunflowerTheme
还可以接受一个名为 darkTheme
的布尔值参数,如果系统处于深色模式,该参数默认为 true
,否则为 false
。使用此参数,我们可以将正确的颜色值传递给 MaterialTheme
,以匹配当前设置的系统主题。
Theme.kt
kotlin
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColorScheme(
primary = colorResource(id = R.color.sunflower_green_500),
primaryContainer = colorResource(id = R.color.sunflower_green_700),
secondary = colorResource(id = R.color.sunflower_yellow_500),
background = colorResource(id = R.color.sunflower_green_500),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
)
val darkColors = darkColorScheme(
primary = colorResource(id = R.color.sunflower_green_100),
primaryContainer = colorResource(id = R.color.sunflower_green_200),
secondary = colorResource(id = R.color.sunflower_yellow_300),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
onBackground = colorResource(id = R.color.sunflower_black),
surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
onSurface = colorResource(id = R.color.sunflower_white),
)
val colors = if (darkTheme) darkColors else lightColors
MaterialTheme(
colorScheme = colors,
content = content
)
}
如需使用此库,请不要使用 MaterialTheme
,改为使用 SunflowerTheme
。例如,在 PlantDetailFragment
中:
PlantDetailFragment.kt
kotlin
class PlantDetailFragment : Fragment() {
...
composeView.apply {
...
setContent {
SunflowerTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
此外还有 PlantDetailDescription.kt
文件中的所有预览可组合项:
PlantDetailDescription.kt
kotlin
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
@Preview
@Composable
private fun PlantNamePreview() {
SunflowerTheme {
PlantName("Apple")
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
SunflowerTheme {
PlantWatering(7)
}
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
SunflowerTheme {
PlantDescription("HTML<br><br>description")
}
}

您还可以在深色主题中预览界面,方法是创建新函数并将 Configuration.UI_MODE_NIGHT_YES
传递给预览的 uiMode
:
kotlin
import android.content.res.Configuration
...
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}

12. 测试
将植物详情界面的各个部分迁移到 Compose 之后,务必要进行测试,确保您没有损坏任何内容。
注意:在真实应用中,如果没有测试,则不应该重写旧代码。在将代码迁移到 Compose 时,您还应该重构测试并确保测试结果合格。
在 Sunflower 中,位于 androidTest
文件夹的 PlantDetailFragmentTest
用于测试应用的某些功能。请打开该文件并查看当前的代码:
testPlantName
用于检查界面上的植物名称testShareTextIntent
用于检查点按分享按钮后是否触发了正确的 intent
当 activity 或 fragment 使用 Compose 时,您不需要使用 ActivityScenarioRule
,而需要使用 createAndroidComposeRule
,它将 ActivityScenarioRule
与 ComposeTestRule
集成,让您可以测试 Compose 代码。
在 PlantDetailFragmentTest
中,将用法 ActivityScenarioRule
替换为 createAndroidComposeRule
。如果需要使用 activity 规则来配置测试,请使用 createAndroidComposeRule
中的 activityRule
属性,具体代码如下所示:
kotlin
@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {
@Rule
@JvmField
val composeTestRule = createAndroidComposeRule<GardenActivity>()
...
@Before
fun jumpToPlantDetailFragment() {
populateDatabase()
composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
activity = gardenActivity
val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
}
}
...
}
如果您运行测试,testPlantName
会失败!testPlantName
检查界面上是否存在 TextView。不过,您已将这部分的界面迁移到 Compose。因此,您需要改用 Compose 断言:
kotlin
@Test
fun testPlantName() {
composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}
如果运行测试,您会看到所有测试均会通过。

13. 恭喜
原始 Sunflower GitHub 项目的 compose
分支会将植物详细信息界面完全迁移到 Compose。除了您在此 Codelab 中完成的操作之外,该分支还会模拟 CollapsingToolbarLayout 的行为。这些行为包括:
- 使用 Compose 加载图片
- 动画
- 更出色的尺寸处理
- 以及更多!