迁移到 Jetpack Compose

1. 简介

Compose 和 View 系统可以结合使用。

在此 Codelab 中,您需要将 Sunflower 的部分植物详情界面迁移到 Compose。我们创建了项目副本,这样一来您可以尝试将一个真实的应用迁移到 Compose。

完成此 Codelab 后,您将能够继续进行迁移,并根据需要转换 Sunflower 剩余的界面。

如果您在学习此 Codelab 的过程中需要获得更多支持,请查看以下配套学习代码:

学习内容

在此 Codelab 中,您将学习:

  • 您可以遵循的不同迁移路径
  • 如何逐步将应用迁移到 Compose
  • 如何将 Compose 添加到使用 Android View 构建的现有界面
  • 如何在 Compose 中使用 Android View
  • 如何在 Compose 中使用 View 系统中的主题
  • 如何使用 View 系统代码和 Compose 代码测试界面

前提条件

所需条件

2. 规划迁移

如何迁移到 Compose 取决于您和您的团队。要将 Jetpack Compose 集成到现有 Android 应用中,有多种不同的方法。常用的两种迁移策略为:

  • 完全使用 Compose 开发一个新界面
  • 选取一个现有界面,然后逐步迁移其中的各个组件。

新界面中的 Compose

在重构应用代码以适应新技术时,一种常用的方法是在为应用构建的新功能中采用该技术。在这种情况下,适合使用新的界面。如果您需要为应用构建新界面,请考虑使用 Compose,而应用的其余部分可以保留在 View 系统中。

在这种情况下,您需要在这些已迁移功能的边缘实现 Compose 互操作性。

搭配使用 Compose 和 View

对于特定界面,您可以将部分界面迁移到 Compose,让其他部分保留在 View 系统中。例如,您可以迁移 RecyclerView,同时将界面的其余部分保留在 View 系统中。

或者,使用 Compose 作为外部布局,并使用 Compose 中可能没有的一些现有 View,比如 MapView 或 AdView。

完成迁移

将全部 fragment 或界面迁移到 Compose,一次迁移一个。这种方式最为简单,但比较粗放。

在此 Codelab 中的任务?

在此 Codelab 中,您将逐步把 Sunflower 的植物详情界面迁移到 Compose,将 Compose 和 View 结合起来。之后,您将掌握足够的知识,可以在需要时继续进行迁移。

3. 准备工作

获取代码

从 GitHub 获取 Codelab 代码:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

或者,您可以下载代码库 Zip 文件:

打开 Android Studio

此 Codelab 需要使用 Android Studio Bumblebee

运行示例应用

您刚刚下载的代码包含提供的所有 Compose Codelab 的代码。为了完成此 Codelab,请在 Android Studio 中打开 MigrationCodelab 项目。

在此 Codelab 中,您需要将 Sunflower 的植物详情界面迁移到 Compose。点按植物列表界面中显示的某个植物,即可打开植物详情界面。

bb6fcf50b2899894.png

项目设置

该项目在多个 git 分支中构建而成:

  • main 是您签出或下载的分支,也是本 Codelab 的起点。
  • end 包含本 Codelab 的解决方案。

建议您从 main 分支中的代码着手,按照自己的节奏逐步完成 Codelab。

在本 Codelab 中,系统会为您显示需要添加到项目的代码段。在某些地方,您还需要移除代码,我们将在代码段的注释中明确标出这部分内容。

如需使用 Git 获取 end 分支,请使用以下命令:

$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs

或从此处下载解决方案代码:

常见问题解答

4. Sunflower 中的 Compose

Compose 已添加到您从 main 分支下载的代码中。不过,我们先来了解一下运行这些代码需要具备哪些条件。

打开 app/build.gradle(或 build.gradle (Module: compose-migration.app))文件后,请查看该文件如何导入 Compose 依赖项,以及如何使用 buildFeatures { compose true } 标记,从而让 Android Studio 能够运行 Compose。

app/build.gradle

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        ...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion rootProject.composeVersion
    }
}

dependencies {
    ...
    // Compose
    implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
    implementation "androidx.compose.material:material:$rootProject.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

这些依赖项的版本在根目录下的 build.gradle 文件中定义。

5. 欢迎使用 Compose!

在植物详情界面中,我们需要将对植物的说明迁移到 Compose,同时让界面的总体结构保持完好。这时,您需要遵循“规划迁移”部分中提到的“搭配使用 Compose 和 View”迁移策略。

Compose 需要有宿主 activity 或 fragment 才能呈现界面。在 Sunflower 中,所有界面都使用 fragment,因此您需要使用 ComposeView:这一 Android View 可以使用其 setContent 方法托管 Compose 界面内容。

移除 XML 代码

我们先从迁移开始!打开 fragment_plant_detail.xml 并执行以下操作:

  1. 切换到代码视图
  2. 移除 NestedScrollView 中的 ConstraintLayout 代码和嵌套的 TextView(此 Codelab 会在迁移各项内容时比较和引用 XML 代码,将代码注释掉会非常有用)
  3. 添加一个 ComposeView,它会改为托管 Compose 代码,并以 compose_view 作为视图 ID

fragment_plant_detail.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 代码

现在,您可以开始将植物详情界面迁移到 Compose 了!

在整个 Codelab 中,您都需要将 Compose 代码添加到 plantdetail 文件夹下的 PlantDetailDescription.kt 文件中。打开该文件,看看项目中是否有占位符 "Hello Compose!" 文本。

plantdetail/PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Text("Hello Compose")
}

我们从在上一步中添加的 ComposeView 中调用此可组合项,即可在界面上显示此内容。打开 plantdetail/PlantDetailFragment.kt

界面使用的是数据绑定,因此您可以直接访问 composeView 并调用 setContent,以便在界面上显示 Compose 代码。您需要在 MaterialTheme 内调用 PlantDetailDescription 可组合项,因为 Sunflower 使用的是 Material Design。

plantdetail/PlantDetailFragment.kt

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()
                }
            }
        }
        ...
    }
}

如果您运行该应用,界面上会显示“Hello Compose!”。

66f3525ecf6669e0.png

6. 使用 XML 创建可组合项

我们首先迁移植物的名称。更确切地说,就是您在 fragment_plant_detail.xml 中移除的 ID 为 @+id/plant_detail_nameTextView。XML 代码如下:

<TextView
    android:id="@+id/plant_detail_name"
    ...
    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"
    ... />

请查看它是否为 textAppearanceHeadline5 样式,水平外边距为 8.dp,以及是否在界面上水平居中。不过,要显示的标题是从由代码库层的 PlantDetailViewModel 公开的 LiveData 中观察到的。

如何观察 LiveData 将在稍后介绍,因此先假设我们有可用的名称,并以参数形式将其传递到我们在 PlantDetailDescription.kt 文件中创建的新 PlantName 可组合项。稍后,将从 PlantDetailDescription 可组合项调用此可组合项。

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

预览如下:

d09fe886b98bde91.png

其中:

  • Text 的样式为 MaterialTheme.typography.h5,它从 XML 代码映射到 textAppearanceHeadline5
  • 修饰符会修饰 Text,以将其调整为类似于 XML 版本:
  • fillMaxWidth 修饰符对应于 XML 代码中的 android:layout_width="match_parent"
  • margin_small 的水平 padding,其值是使用 dimensionResource 辅助函数从 View 系统获取的。
  • wrapContentWidth 水平对齐 Text

7. ViewModel 和 LiveData

现在,我们将标题连接到界面。如需执行此操作,您需要使用 PlantDetailViewModel 加载数据。为此,Compose 集成了 ViewModelLiveData

ViewModel

由于 fragment 中使用了 PlantDetailViewModel 的实例,因此我们可以将其作为参数传递给 PlantDetailDescription,就这么简单。

打开 PlantDetailDescription.kt 文件,然后将 PlantDetailViewModel 参数添加到 PlantDetailDescription

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    ...
}

现在,请在从 fragment 调用此可组合项时传递 ViewModel 实例:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

有了 LiveData,您已有权访问 PlantDetailViewModelLiveData<Plant> 字段,以获取植物的名称。

如需从可组合项观察 LiveData,请使用 LiveData.observeAsState() 函数。

由于 LiveData 发出的值可以为 null,因此您需要将其用法封装在 null 检查中。有鉴于此,以及为了实现可重用性,最好将 LiveData 的使用和监听拆分到不同的可组合项中。因此,请创建一个名为 PlantDetailContent 的新可组合项,用于显示 Plant 信息。

基于以上原因,添加 LiveData 观察后,PlantDetailDescription.kt 文件将如下所示。

PlantDetailDescription.kt

@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

3e47e682cf518c71.png

现在,您已完成在 Compose 中显示植物名称所需的所有 ViewModel 连接。在接下来的几部分中,您将构建其余可组合项,并以类似的方式将它们连接到 ViewModel。

8. 更多 XML 代码迁移

现在,我们可以更轻松地将界面中缺少的内容补充完整:浇水信息和植物说明。按照前面介绍的 XML 代码方法,您已经可以迁移界面的其余部分了。

您之前从 fragment_plant_detail.xml 移除的浇水信息 XML 代码由两个 ID 为 plant_watering_headerplant_watering 的 TextView 组成。

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

与您之前的操作类似,请创建一个名为 PlantWatering 的新可组合项并添加 Text,以在界面上显示浇水信息:

PlantDetailDescription.kt

@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.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = LocalContext.current.resources.getQuantityString(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

预览如下:

6f6c17085801a518.png

需要注意以下几点:

  • 由于 Text 可组合项会共享水平内边距和对齐修饰,因此您可以将修饰符分配给局部变量(即 centerWithPaddingModifier),以重复使用修饰符。修饰符是标准的 Kotlin 对象,因此可以重复使用。
  • Compose 的 MaterialThemeplant_watering_header 中使用的 colorAccent 不完全匹配。现在,我们可以使用将在主题设置部分中加以改进的 MaterialTheme.colors.primaryVariant

我们将各个部分组合在一起,然后同样从 PlantDetailContent 调用 PlantWatering。我们一开始移除的 ConstraintLayout XML 代码的外边距为 16.dp,我们需要将该值添加到 Compose 代码中。

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

请在 PlantDetailContent 中创建一个 Column 以同时显示名称和浇水信息,并将其作为内边距。另外,为了确保背景颜色和所用的文本颜色均合适,请添加 Surface 用于处理这种设置。

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

刷新预览后,您会看到以下内容:

56626a7118ce075c.png

9. Compose 代码中的 View

现在,我们来迁移植物说明。fragment_plant_detail.xml 中的代码具有包含 app:renderHtml="@{viewModel.plant.description}"TextView,用于告知 XML 在界面上显示哪些文本。renderHtml 是一个绑定适配器,可在 PlantDetailBindingAdapters.kt 文件中找到。该实现使用 HtmlCompat.fromHtmlTextView 上设置文本!

但是,Compose 目前不支持 Spanned 类,也不支持显示 HTML 格式的文本。因此,我们需要在 Compose 代码中使用 View 系统中的 TextView 来绕过此限制。

由于 Compose 目前还无法呈现 HTML 代码,因此您需要使用 AndroidView API 程序化地创建一个 TextView,从而实现此目的。

AndroidView 接受 View 作为参数,并在 View 已膨胀时为您提供回调。

为此,请创建新的 PlantDescription 可组合项。此可组合项使用我们刚刚在 lambda 中保存的 TextView 调用 AndroidView。在 factory 回调中,请初始化使用给定 Context 来回应 HTML 交互的 TextView。在 update 回调中,用已保存的 HTML 格式的说明设置文本。

PlantDetailDescription.kt

@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")
    }
}

预览:

deea1d191e9087b4.png

请注意,htmlDescription 会记住作为参数传递的指定 description 的 HTML 说明。如果 description 参数发生变化,系统会再次执行 remember 中的 htmlDescription 代码。

同样,如果 htmlDescription 发生变化,AndroidView 更新回调会重组。在回调中读取的任何状态都会导致重组。

我们将 PlantDescription 添加到 PlantDetailContent 可组合项,并更改预览代码,以便同样显示 HTML 说明:

PlantDetailDescription.kt

@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)
    }
}

预览如下:

7843a8d6c781c244.png

现在,您已将原始 ConstraintLayout 中的所有内容迁移到 Compose。您可以运行该应用,检查其是否按预期运行。

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

默认情况下,只要 ComposeView 与窗口分离,Compose 就会处理组合。如果 fragment 中使用了 ComposeView,默认设置是不可取的,这有多种原因:

  • 组合必须遵循 fragment 的视图生命周期,Compose 界面 View 类型才能保存状态,并且
  • 在发生过渡或窗口过渡时让 Compose 界面元素保留在界面上。在过渡期间,ComposeView 本身仍然可见,即使其与窗口分离也是如此。

您可以手动调用 AbstractComposeView.disposeComposition 方法来手动处理组合。或者,如需在不再需要组合时自动处理组合,请通过调用 setViewCompositionStrategy 方法设置其他策略或创建自己的策略。

在 fragment 的 LifecycleOwner 被销毁时,使用 DisposeOnViewTreeLifecycleDestroyed 策略处理组合。

由于 PlantDetailFragment 包含进入和退出过渡(如需了解详情,请查看 nav_garden.xml),并且我们稍后会在 Compose 中使用 View 类型,因此我们需要确保 ComposeView 使用 DisposeOnViewTreeLifecycleDestroyed 策略。不过,只要是在 fragment 中使用 ComposeView最好都设置此策略。

plantdetail/PlantDetailFragment.kt

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. 互操作性主题设置

我们已将植物详情的文本内容迁移到 Compose。不过,您可能已经注意到,Compose 使用的主题颜色有误。当植物名称应该使用绿色时,它使用的是紫色。

在这个迁移的早期阶段,您可能需要 Compose 继承 View 系统中可用的主题,而不是从头开始在 Compose 中重新编写您自己的 Material 主题。Material 主题可与 Compose 附带的所有 Material Design 组件完美配合使用。

如需在 Compose 中重复使用 View 系统的 Material Design 组件 (MDC) 主题,您可以使用 compose-theme-adapterMdcTheme 函数将自动读取主机上下文的 MDC 主题,并代表您将它们传递给 MaterialTheme,以用于浅色和深色主题。即使您只需要适用于此 Codelab 的主题颜色,该库也会读取 View 系统的形状和排版。

该库已包含在 app/build.gradle 文件中,如下所示:

...
dependencies {
    ...
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

如需使用此库,请将 MaterialTheme 的用法替换为 MdcTheme。例如,在 PlantDetailFragment 中:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            MdcTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

此外还有 PlantDetailDescription.kt 文件中的所有预览可组合项:

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    MdcTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MdcTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MdcTheme {
        PlantDescription("HTML<br><br>description")
    }
}

在预览中您可以看到,MdcTheme 会从 styles.xml 文件中的主题中提取颜色。

886d7eaea611f4eb.png

您还可以在深色主题中预览界面,方法是创建新函数并将 Configuration.UI_MODE_NIGHT_YES 传递给预览的 uiMode

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, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

预览如下:

cfe11c109ff19eeb.png

如果您运行应用,它在浅色主题和深色主题下的行为都将与迁移前完全相同:

c99216fc77699dd7.gif

12. 测试

将植物详情界面的部分内容迁移到 Compose 之后,务必要进行测试,以确保您没有损坏任何内容。

在 Sunflower 中,位于 androidTest 文件夹的 PlantDetailFragmentTest 用于测试应用的某些功能。请打开该文件并查看当前的代码:

  • testPlantName 用于检查界面上的植物名称
  • testShareTextIntent 用于检查点按分享按钮后是否触发了正确的 intent

当 activity 或 fragment 使用 Compose 时,您不需要使用 ActivityScenarioRule,而需要使用 createAndroidComposeRule,它将 ActivityScenarioRuleComposeTestRule 集成,让您可以测试 Compose 代码。

PlantDetailFragmentTest 中,将用法 ActivityScenarioRule 替换为 createAndroidComposeRule。如果需要使用 activity 规则来配置测试,请使用 createAndroidComposeRule 中的 activityRule 属性,具体代码如下所示:

@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 断言:

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

如果运行测试,您会看到所有测试均会通过。

dd59138fac1740e4.png

13. 恭喜

恭喜,您已成功完成此 Codelab!

原始 Sunflower GitHub 项目的 compose 分支会将植物详细信息界面完全迁移到 Compose。除了您在此 Codelab 中完成的操作之外,该分支还会模拟 CollapsingToolbarLayout 的行为。这些行为包括:

  • 使用 Compose 加载图片
  • 动画
  • 更出色的尺寸处理
  • 等等!

后续操作

请查看 Compose 开发者在线课程中的其他 Codelab:

深入阅读