1. 简介
Compose 和 View 系统可以并行工作。
在本 Codelab 中,您将把 Sunflower 的植物详细信息屏幕的部分内容迁移到 Compose。我们创建了一个项目的副本,供您尝试将真实的应用程序迁移到 Compose。
在本 Codelab 结束时,您将能够继续迁移,并根据需要将 Sunflower 的其余屏幕转换为 Compose。
如果您在浏览本 Codelab 时需要更多支持,请查看以下代码协作
您将学到什么
在本 Codelab 中,您将学习
- 您可以遵循的不同迁移路径
- 如何将应用程序逐步迁移到 Compose
- 如何将 Compose 添加到使用 View 构建的现有屏幕中
- 如何在 Compose 内部使用 View
- 如何在 Compose 中创建主题
- 如何测试同时使用 View 和 Compose 编写的混合屏幕
先决条件
- 熟悉 Kotlin 语法,包括 lambda 表达式
- 了解 Compose 基础知识
您需要什么
2. 迁移策略
Jetpack Compose 从一开始就设计了与 View 的互操作性。为了迁移到 Compose,我们建议进行增量迁移,其中 Compose 和 View 在您的代码库中共存,直到您的应用程序完全使用 Compose。
推荐的 迁移策略 是:
- 使用 Compose 构建新屏幕
- 在构建功能时,识别可重用元素,并开始创建一个通用 UI 组件库
- 一次替换一个现有功能
使用 Compose 构建新屏幕
使用 Compose 构建涵盖整个屏幕的新功能是推动您采用 Compose 的最佳方式。使用这种策略,您可以添加功能并利用 Compose 的优势,同时仍然满足公司的业务需求。
新功能可能会涵盖整个屏幕,在这种情况下,整个屏幕都将使用 Compose。如果您使用的是基于 Fragment 的导航,这意味着您将创建一个新的 Fragment,并将它的内容放在 Compose 中。
您也可以在现有屏幕中引入新功能。在这种情况下,View 和 Compose 将在同一个屏幕上共存。例如,假设您要添加的功能是 RecyclerView 中的一种新的视图类型。在这种情况下,新的视图类型将使用 Compose,而其他项目保持不变。
构建一个通用 UI 组件库
在使用 Compose 构建功能时,您会很快意识到,最终会构建一个组件库。您需要识别可重用组件,以促进整个应用程序的重用,以便共享组件具有单一的事实来源。您构建的新功能可以依赖于此库。
用 Compose 替换现有功能
除了构建新功能外,您还需要逐步将应用程序中的现有功能迁移到 Compose。您如何处理取决于您,但这里有一些不错的候选者:
- 简单屏幕 - 应用程序中包含少量 UI 元素和动态性的简单屏幕,例如欢迎屏幕、确认屏幕或设置屏幕。这些是迁移到 Compose 的不错候选者,因为可以使用少量代码完成。
- 混合 View 和 Compose 屏幕 - 已经包含一些 Compose 代码的屏幕是另一个不错的候选者,因为您可以继续逐步迁移该屏幕中的元素。如果您有一个屏幕只在 Compose 中包含一个子树,您可以继续迁移树的其他部分,直到整个 UI 都在 Compose 中。这被称为迁移的自下而上方法。
本 Codelab 中的方法
在本 Codelab 中,您将对 Sunflower 的植物详细信息屏幕进行增量迁移到 Compose,使 Compose 和 View 协同工作。之后,您将了解足够的知识,以便根据需要继续迁移。
3. 设置
获取代码
从 GitHub 获取 Codelab 代码
$ git clone https://github.com/android/codelab-android-compose
或者,您可以将存储库下载为 Zip 文件
运行示例应用程序
您刚刚下载的代码包含所有可用 Compose Codelab 的代码。要完成本 Codelab,请在 Android Studio 中打开MigrationCodelab
项目。
在本 Codelab 中,您将把 Sunflower 的植物详细信息屏幕迁移到 Compose。您可以通过点击植物列表屏幕中的一种植物来打开植物详细信息屏幕。
项目设置
该项目是在多个 Git 分支中构建的
main
分支是 Codelab 的起点。end
分支包含本 Codelab 的解决方案。
我们建议您从 main
分支中的代码开始,并按照自己的节奏逐步完成 Codelab。
在 Codelab 过程中,您将看到需要添加到项目的代码片段。在某些地方,您还需要删除代码片段注释中明确提到的代码。
要使用 Git 获取 end
分支,请在 cd
到MigrationCodelab
项目的目录中,然后使用以下命令:
$ git checkout end
或者,您可以从这里下载解决方案代码
常见问题
4. Sunflower 中的 Compose
Compose 已经添加到您从 main
分支下载的代码中。但是,让我们看看它正常工作需要什么。
如果您打开应用程序级别的 build.gradle
文件,请查看它如何导入 Compose 依赖项,并使用 buildFeatures { compose true }
标志使 Android Studio 能够与 Compose 一起使用。
app/build.gradle
android {
//...
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
//...
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
dependencies {
//...
// Compose
def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
implementation(composeBom)
androidTestImplementation(composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material:material"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
//...
}
这些依赖项的版本在项目级别的 build.gradle
文件中定义。
5. Hello Compose!
在植物详细信息屏幕中,我们将把植物的描述迁移到 Compose,同时保持屏幕的整体结构不变。
Compose 需要一个宿主 Activity 或 Fragment 才能渲染 UI。在 Sunflower 中,由于所有屏幕都使用 Fragment,因此您将使用 ComposeView
:一个 Android View,可以使用它的 setContent
方法托管 Compose UI 内容。
删除 XML 代码
让我们开始迁移!打开 fragment_plant_detail.xml
并执行以下操作:
- 切换到代码视图
- 删除
ConstraintLayout
代码,以及NestedScrollView
内部的 4 个嵌套的TextView
(Codelab 将在迁移单个项目时比较和引用 XML 代码,将代码注释掉将很有用) - 添加一个
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 中,您将向 plantdetail
文件夹下的 PlantDetailDescription.kt
文件添加 Compose 代码。打开它,看看项目中是否已经有一个占位符 "Hello Compose"
文本。
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
让我们通过从我们在上一步中添加的 ComposeView
调用此可组合项来在屏幕上显示它。打开 PlantDetailFragment.kt
。
由于该屏幕使用的是 数据绑定,因此您可以直接访问 composeView
,并调用 setContent
以在屏幕上显示 Compose 代码。在 MaterialTheme
内调用 PlantDetailDescription
可组合项,因为 Sunflower 使用的是 Material Design。
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
" 显示在屏幕上。
6. 从 XML 创建可组合项
让我们从迁移植物的名称开始。更确切地说,是您在 fragment_plant_detail.xml
中删除的 ID 为 @+id/plant_detail_name
的 TextView
。以下是 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
来自存储库层。
由于观察 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")
}
}
带预览
其中
Text
的样式是MaterialTheme.typography.h5
,类似于 XML 代码中的textAppearanceHeadline5
。- 修饰符用于修饰 Text,使其看起来像 XML 版本
fillMaxWidth
修饰符用于使它占用可用的最大宽度。此修饰符对应于 XML 代码中layout_width
属性的match_parent
值。- 使用
padding
修饰符,以便应用margin_small
的水平填充值。这对应于 XML 中的marginStart
和marginEnd
声明。margin_small
值也是使用dimensionResource
辅助函数获取的现有尺寸资源。 - 使用
wrapContentWidth
修饰符将文本水平居中对齐。这类似于在 XML 中将gravity
设置为center_horizontal
。
7. ViewModels 和 LiveData
现在,让我们将标题连接到屏幕上。为此,您需要使用 PlantDetailViewModel
加载数据。为此,Compose 集成了 ViewModel 和 LiveData。
ViewModels
由于在 Fragment 中使用了 PlantDetailViewModel
的实例,我们可以将其作为参数传递给 PlantDetailDescription
,然后就完成了。
打开 PlantDetailDescription.kt
文件,并将 PlantDetailViewModel
参数添加到 PlantDetailDescription
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
//...
}
现在,在从片段调用此可组合项时,传递 ViewModel 的实例
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
...
composeView.setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
LiveData
有了这个,您就可以访问 PlantDetailViewModel
的 LiveData<Plant>
字段以获取植物名称。
要从可组合项中观察 LiveData,请使用 LiveData.observeAsState()
函数。
由于 LiveData 发出的值可以为 null
,因此您需要将它的使用包装在 null
检查中。因此,为了可重用性,最好将 LiveData 的使用和监听拆分为不同的可组合项。所以,让我们创建一个名为 PlantDetailContent
的新可组合项,它将显示 Plant
信息。
完成这些更新后,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
现在,您已经连接了 ViewModel,以便在 Compose 中显示植物名称。在接下来的几节中,您将构建其余的可组合项,并以类似的方式将它们连接到 ViewModel。
8. 更多 XML 代码迁移
现在,我们可以更容易地完成 UI 中缺少的部分:浇水信息和植物描述。按照之前类似的方法,您已经可以迁移屏幕的其余部分。
之前从 fragment_plant_detail.xml
中删除的浇水信息 XML 代码由两个 TextView 组成,它们的 ID 分别为 plant_watering_header
和 plant_watering
。
<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
@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.colors.primaryVariant,
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
)来重用 Modifier。由于修饰符是普通的 Kotlin 对象,因此您可以这样做。 - Compose 的
MaterialTheme
没有与plant_watering_header
中使用的colorAccent
完全匹配。目前,让我们使用MaterialTheme.colors.primaryVariant
,您将在互操作主题部分中对其进行改进。 - 在 Compose 1. 2.1 中,使用
pluralStringResource
需要选择加入ExperimentalComposeUiApi
。在 Compose 的未来版本中,可能不再需要这样做。
让我们将所有部分连接在一起,并从 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)
}
}
}
如果您刷新预览,您将看到以下内容
9. Compose 代码中的视图
现在,让我们迁移植物描述。fragment_plant_detail.xml
中的代码有一个 TextView
,它的 app:renderHtml="@{viewModel.plant.description}"
用于告诉 XML 在屏幕上显示什么文本。 renderHtml
是一个绑定适配器,您可以在 PlantDetailBindingAdapters.kt
文件中找到它。实现使用 HtmlCompat.fromHtml
在 TextView
上设置文本!
但是,Compose 目前不支持 Spanned
类或显示 HTML 格式的文本。因此,我们需要在 Compose 代码中使用视图系统中的 TextView
来绕过此限制。
由于 Compose 无法渲染 HTML 代码,因此您将以编程方式创建一个 TextView
来使用 AndroidView
API 来完成此操作。
AndroidView
允许您在其 factory
lambda 中构造一个 View
。它还提供了一个 update
lambda,该 lambda 在视图被膨胀后以及在随后的重新组合时被调用。
让我们通过创建一个名为 PlantDescription
的新可组合项来实现。此可组合项调用 AndroidView
,它在其 factory
lambda 中构造一个 TextView
。在 factory
lambda 中,初始化一个 TextView
,该 TextView 显示 HTML 格式的文本,然后将 movementMethod
设置为 LinkMovementMethod
的实例。最后,在 update
lambda 中,将 TextView
的文本设置为 htmlDescription
。
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")
}
}
预览
请注意,htmlDescription
会记住为给定的 description
传递的 HTML 描述作为参数。如果 description
参数发生更改,remember
内部的 htmlDescription
代码将再次执行。
因此,如果 htmlDescription
发生更改,AndroidView
更新回调将重新组合。在 update
lambda 中读取的任何状态都会导致重新组合。
让我们将 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)
}
}
带预览
至此,您已经将原始 ConstraintLayout
中的所有内容迁移到 Compose。您可以运行应用程序以检查它是否按预期工作。
10. ViewCompositionStrategy
只要 ComposeView
与窗口分离,Compose 就会处理 Composition。当 ComposeView
用于片段时,这会产生两个不利因素
- Composition 必须遵循 片段的视图生命周期 才能为 Compose UI
View
类型保存状态。 - 当发生转换时,底层的
ComposeView
将处于分离状态。但是,Compose UI 元素在这些转换过程中仍然可见。
要修改此行为,请使用适当的 ViewCompositionStrategy
调用 setViewCompositionStrategy
,使其遵循片段的视图生命周期。具体来说,您需要使用 DisposeOnViewTreeLifecycleDestroyed
策略,在片段的 LifecycleOwner
被销毁时处理 Composition。
由于 PlantDetailFragment
有进入和退出转换(有关更多信息,请查看 nav_garden.xml
),并且我们将在以后在 Compose 中使用 View
类型,因此我们需要确保 ComposeView
使用 DisposeOnViewTreeLifecycleDestroyed
策略。尽管如此,在片段中使用 ComposeView
时,始终设置此策略是 最佳做法。
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 没有使用正确的主题颜色。它在植物名称中使用紫色,而应该使用绿色。
要使用正确的主题颜色,您需要通过定义自己的主题并提供主题的颜色来自定义 MaterialTheme
。
自定义 MaterialTheme
要创建自己的主题,请打开 theme
包中的 Theme.kt
文件。Theme.kt
定义了一个名为 SunflowerTheme
的可组合项,它接受一个内容 lambda 并将其传递给 MaterialTheme
。
它目前还没有做任何有趣的事情——你将在下一步对其进行自定义。
Theme.kt
import androidx.compose.material.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
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColors(
primary = colorResource(id = R.color.sunflower_green_500),
primaryVariant = 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 = darkColors(
primary = colorResource(id = R.color.sunflower_green_100),
primaryVariant = 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(
colors = colors,
content = content
)
}
要使用它,请将 MaterialTheme
的用法替换为 SunflowerTheme
。例如,在 PlantDetailFragment
中
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
composeView.apply {
...
setContent {
SunflowerTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
以及 PlantDetailDescription.kt
文件中的所有预览可组合项
PlantDetailDescription.kt
@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")
}
}
如预览所示,颜色现在应与 Sunflower 主题的颜色相匹配。
你还可以通过创建一个新函数并将 Configuration.UI_MODE_NIGHT_YES
传递给预览的 uiMode
来预览深色主题中的 UI
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 后,测试至关重要,以确保你没有破坏任何东西。
在 Sunflower 中,位于 androidTest
文件夹中的 PlantDetailFragmentTest
测试了应用程序的一些功能。打开文件并查看当前代码
testPlantName
检查屏幕上植物的名称testShareTextIntent
检查在点击分享按钮后是否触发了正确的 Intent
当活动或片段使用 Compose 时,你需要使用 createAndroidComposeRule
,而不是使用 ActivityScenarioRule
。此规则集成了 ActivityScenarioRule
和一个 ComposeTestRule
,后者允许你测试 Compose 代码。
在 PlantDetailFragmentTest
中,将 ActivityScenarioRule
的用法替换为 createAndroidComposeRule
。当需要使用活动规则来配置测试时,请使用 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。但是,你已将 UI 的该部分迁移到 Compose。因此,你需要使用 Compose 断言来代替
@Test
fun testPlantName() {
composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}
如果你运行测试,你将看到所有测试都通过了。
13. 恭喜
恭喜你,你已成功完成此 Codelab!
原始 Sunflower github 项目的 compose
分支 将植物详细信息屏幕完全迁移到 Compose。除了你在本 Codelab 中所做的操作之外,它还模拟了 CollapsingToolbarLayout 的行为。这包括
- 使用 Compose 加载图像
- 动画
- 更好的尺寸处理
- 等等!
下一步是什么?
查看 Compose 学习路径 中的其他 Codelab