1. 简介
在之前的 Codelab 中,您开始使用窗口大小类和实现动态导航来转换 Reply 应用以使其具有自适应性。这些功能是重要的基础,也是构建适用于所有屏幕尺寸应用的第一步。如果您错过了 构建具有动态导航的自适应应用 Codelab,强烈建议您返回并从那里开始。
在本 Codelab 中,您将基于您学到的概念进一步在您的应用中实现自适应布局。您将实现的自适应布局是规范布局的一部分 - 一组用于大屏幕显示的常用模式。您还将了解更多工具和测试技术,以帮助您快速构建健壮的应用。
先决条件
- 已完成 构建具有动态导航的自适应应用 Codelab
- 熟悉 Kotlin 编程,包括类、函数和条件语句
- 熟悉
ViewModel
类 - 熟悉
Composable
函数 - 使用 Jetpack Compose 构建布局的经验
- 在设备或模拟器上运行应用的经验
- 使用
WindowSizeClass
API 的经验
您将学到什么
- 如何使用 Jetpack Compose 创建列表视图模式的自适应布局
- 如何为不同的屏幕尺寸创建预览
- 如何测试多个屏幕尺寸的代码
您将构建什么
- 您将继续更新 Reply 应用,使其适用于所有屏幕尺寸。
完成后的应用将如下所示
您需要什么
- 一台连接互联网的电脑、一个网络浏览器和 Android Studio
- 访问 GitHub
下载启动代码
要开始,请下载启动代码
或者,您可以克隆代码的 GitHub 存储库
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git $ cd basic-android-kotlin-compose-training-reply-app $ git checkout nav-update
您可以在 Reply
GitHub 存储库中浏览启动代码。
2. 不同屏幕尺寸的预览
为不同的屏幕尺寸创建预览
在 构建具有动态导航的自适应应用 Codelab 中,您学习了如何使用预览组合函数来帮助您的开发过程。对于自适应应用,最佳实践是创建多个预览以显示应用在不同屏幕尺寸上的效果。使用多个预览,您可以同时查看所有屏幕尺寸上的更改。此外,预览还可以作为其他开发人员审查您的代码的文档,以查看您的应用是否与不同的屏幕尺寸兼容。
以前,您只有一个支持紧凑屏幕的预览。接下来,您将添加更多预览。
要为中等和扩展屏幕添加预览,请完成以下步骤
- 通过在
Preview
注解参数中设置中等widthDp
值,并在ReplyApp
组合函数的参数中指定WindowWidthSizeClass.Medium
值,来添加中等屏幕的预览。
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Medium)
}
}
}
...
- 通过在
Preview
注解参数中设置大widthDp
值,并在ReplyApp
组合函数的参数中指定WindowWidthSizeClass.Expanded
值,来添加另一个扩展屏幕的预览。
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
}
}
}
...
- 构建预览以查看以下内容
3. 实现自适应内容布局
列表-详情视图简介
您可能会注意到,在扩展屏幕上,内容看起来被拉伸了,并没有很好地利用可用的屏幕空间。
您可以通过应用其中一个 规范布局 来改进此布局。规范布局是大屏幕组合,用作设计和实现的起点。您可以使用三个可用的布局来指导您如何在应用中组织常用元素,列表视图、支持面板和 Feed。每个布局都考虑了常见用例和组件,以解决应用跨屏幕尺寸和断点自适应的期望和用户需求。
对于 Reply 应用,让我们实现 列表-详情视图,因为它最适合浏览内容并快速查看详细信息。使用列表-详情视图布局,您将在电子邮件列表屏幕旁边创建另一个窗格以显示电子邮件详细信息。此布局允许您使用可用的屏幕向用户显示更多信息,并使您的应用更具生产力。
实现列表-详情视图
要为扩展屏幕实现列表-详情视图,请完成以下步骤
- 为了表示不同类型的 content 布局,在
WindowStateUtils.kt
上,为不同的 content 类型创建一个新的Enum
类。当使用扩展屏幕时,使用LIST_AND_DETAIL
值,否则使用LIST_ONLY
值。
WindowStateUtils.kt
...
enum class ReplyContentType {
LIST_ONLY, LIST_AND_DETAIL
}
...
- 在
ReplyApp.kt
上声明contentType
变量,并为各种窗口大小分配适当的contentType
,以帮助根据屏幕尺寸确定适当的内容类型选择。
ReplyApp.kt
...
import com.example.reply.ui.utils.ReplyContentType
...
val navigationType: ReplyNavigationType
val contentType: ReplyContentType
when (windowSize) {
WindowWidthSizeClass.Compact -> {
...
contentType = ReplyContentType.LIST_ONLY
}
WindowWidthSizeClass.Medium -> {
...
contentType = ReplyContentType.LIST_ONLY
}
WindowWidthSizeClass.Expanded -> {
...
contentType = ReplyContentType.LIST_AND_DETAIL
}
else -> {
...
contentType = ReplyContentType.LIST_ONLY
}
}
...
接下来,您可以使用 contentType
值在 ReplyAppContent
组合函数中创建不同的分支布局。
- 在
ReplyHomeScreen.kt
中,将contentType
作为参数添加到ReplyHomeScreen
组合函数中。
ReplyHomeScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
- 将
contentType
值传递给ReplyHomeScreen
组合函数。
ReplyApp.kt
...
ReplyHomeScreen(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
...
- 将
contentType
作为ReplyAppContent
组合函数的参数。
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- 将
contentType
值传递给两个ReplyAppContent
组合函数。
ReplyHomeScreen.kt
...
ReplyAppContent(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackButtonClicked = onDetailScreenBackPressed,
modifier = modifier
)
}
}
...
当 contentType
为 LIST_AND_DETAIL
时,让我们显示完整的列表和详情屏幕,或者当 contentType
为 LIST_ONLY
时,仅显示列表电子邮件内容。
- 在
ReplyHomeScreen.kt
中,在ReplyAppContent
组合函数上添加一个if/else
语句,当contentType
值为LIST_AND_DETAIL
时显示ReplyListAndDetailContent
组合函数,并在else
分支中显示ReplyListOnlyContent
组合函数。
ReplyHomeScreen.kt
...
Column(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
if (contentType == ReplyContentType.LIST_AND_DETAIL) {
ReplyListAndDetailContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
} else {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
}
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
...
- 删除
replyUiState.isShowingHomepage
条件以显示 永久导航抽屉,因为如果用户使用扩展视图,则无需导航到详细信息视图。
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
...
- 在平板电脑模式下运行您的应用以查看下面的屏幕
改进列表-详情视图的 UI 元素
目前,您的应用在扩展屏幕的主屏幕上显示了一个详细信息窗格。
但是,屏幕包含多余的元素,例如后退按钮、主题标题和额外的填充,因为它最初是为独立的详细信息屏幕设计的。您可以通过简单的调整来改进这一点。
要改进扩展视图的详细信息屏幕,请完成以下步骤
- 在
ReplyDetailsScreen.kt
中,将isFullScreen
变量作为Boolean
参数添加到ReplyDetailsScreen
组合函数中。
此添加允许您在将组合函数用作独立函数时和在主屏幕内使用时进行区分。
ReplyDetailsScreen.kt
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- 在
ReplyDetailsScreen
组合函数内,使用if
语句包装ReplyDetailsScreenTopBar
组合函数,以便它仅在应用处于全屏状态时显示。
ReplyDetailsScreen.kt
...
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.inverseOnSurface)
.padding(top = dimensionResource(R.dimen.detail_card_list_padding_top))
) {
item {
if (isFullScreen) {
ReplyDetailsScreenTopBar(
onBackPressed,
replyUiState,
Modifier
.fillMaxWidth()
.padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
)
)
}
...
您现在可以添加填充。所需的 ReplyEmailDetailsCard
组合函数填充取决于是否将其用作全屏。当您与扩展屏幕中的其他组合函数一起使用 ReplyEmailDetailsCard
时,其他组合函数会提供额外的填充。
- 将
isFullScreen
值传递给ReplyEmailDetailsCard
组合函数。如果屏幕是全屏的,则传递一个具有R.dimen.detail_card_outer_padding_horizontal
水平填充的修饰符,否则传递一个具有R.dimen.detail_card_outer_padding_horizontal
结束填充的修饰符。
ReplyDetailsScreen.kt
...
item {
if (isFullScreen) {
ReplyDetailsScreenTopBar(
onBackPressed,
replyUiState,
Modifier
.fillMaxWidth()
.padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
)
)
}
ReplyEmailDetailsCard(
email = replyUiState.currentSelectedEmail,
mailboxType = replyUiState.currentMailbox,
isFullScreen = isFullScreen,
modifier = if (isFullScreen) {
Modifier.padding(horizontal = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
} else {
Modifier.padding(end = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
}
)
}
...
- 将
isFullScreen
值作为参数添加到ReplyEmailDetailsCard
组合函数中。
ReplyDetailsScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
email: Email,
mailboxType: MailboxType,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- 在
ReplyEmailDetailsCard
组合函数内,仅当应用未处于全屏状态时才显示电子邮件主题文本,因为全屏布局已将电子邮件主题显示为标题。如果是全屏,则添加一个高度为R.dimen.detail_content_padding_top
的间隔符。
ReplyDetailsScreen.kt
...
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.detail_card_inner_padding))
) {
DetailsScreenHeader(
email,
Modifier.fillMaxWidth()
)
if (isFullScreen) {
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.detail_content_padding_top)))
} else {
Text(
text = stringResource(email.subject),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.padding(
top = dimensionResource(R.dimen.detail_content_padding_top),
bottom = dimensionResource(R.dimen.detail_expanded_subject_body_spacing)
),
)
}
Text(
text = stringResource(email.body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
DetailsScreenButtonBar(mailboxType, displayToast)
}
...
- 在
ReplyHomeScreen.kt
中,在ReplyHomeScreen
组合函数内,在创建ReplyDetailsScreen
组合函数作为独立函数时,为isFullScreen
参数传递true
值。
ReplyHomeScreen.kt
...
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
...
- 在平板电脑模式下运行应用并查看以下布局
调整列表-详情视图的后退处理
使用扩展屏幕时,您根本不需要导航到 ReplyDetailsScreen
。相反,您希望应用在用户选择后退按钮时关闭。因此,我们应该调整后退处理程序。
通过将 activity.finish()
函数作为 onBackPressed
参数传递给 ReplyListAndDetailContent
组合函数内的 ReplyDetailsScreen
组合函数来修改后退处理程序。
ReplyHomeContent.kt
...
import android.app.Activity
import androidx.compose.ui.platform.LocalContext
...
val activity = LocalContext.current as Activity
ReplyDetailsScreen(
replyUiState = replyUiState,
modifier = Modifier.weight(1f),
onBackPressed = { activity.finish() }
)
...
4. 验证不同的屏幕尺寸
大屏幕应用质量指南
为了为 Android 用户构建出色且一致的体验,务必牢记质量,并以此为基础构建和测试您的应用。您可以参考核心应用质量指南,了解如何提高应用质量。
要为所有尺寸规格构建高质量的应用,请查看大屏幕应用质量指南。您的应用还必须满足第 3 层 - 大屏幕就绪要求。
手动测试您的应用是否适用于大屏幕
应用质量指南提供了测试设备建议和程序,以检查您的应用质量。让我们来看一个与 Reply 应用相关的测试示例。
上述应用质量指南要求应用在配置更改后保留或恢复其状态。该指南还提供了有关如何测试应用的说明,如下面的图所示
要手动测试 Reply 应用的配置连续性,请完成以下步骤
- 在中等尺寸的设备上运行 Reply 应用,或者如果您使用的是可调整大小的模拟器,则在展开的折叠模式下运行。
- 确保模拟器上的自动旋转设置为开启。
- 向下滚动电子邮件列表。
- 点击电子邮件卡片。例如,打开来自Ali的电子邮件。
- 旋转设备以检查所选电子邮件是否仍与纵向模式下所选电子邮件一致。在此示例中,仍显示来自 Ali 的电子邮件。
- 旋转回纵向模式以检查应用是否仍显示相同的电子邮件。
5. 为自适应应用添加自动化测试
为紧凑屏幕尺寸配置测试
在测试 Cupcake 应用 代码实验室中,您学习了如何创建 UI 测试。现在让我们学习如何为不同的屏幕尺寸创建特定的测试。
在 Reply 应用中,您对不同屏幕尺寸使用不同的导航元素。例如,您希望在用户查看扩展屏幕时看到永久导航抽屉。创建测试以验证各种导航元素(例如底部导航、导航栏和导航抽屉)在不同屏幕尺寸下的存在性非常有用。
要创建测试以验证紧凑屏幕中底部导航元素的存在,请完成以下步骤
- 在测试目录中,创建一个名为
ReplyAppTest.kt
的新 Kotlin 类。 - 在
ReplyAppTest
类中,使用createAndroidComposeRule
创建测试规则,并将ComponentActivity
作为类型参数传递。ComponentActivity
用于访问空活动而不是MainActivity
。
ReplyAppTest.kt
...
class ReplyAppTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...
为了区分屏幕中的导航元素,在ReplyBottomNavigationBar
可组合项中添加testTag
。
- 为导航底部定义字符串资源。
strings.xml
...
<resources>
...
<string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
- 将字符串名称作为
Modifier
的testTag
方法中的testTag
参数添加到ReplyBottomNavigationBar
可组合项中。
ReplyHomeScreen.kt
...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
...
modifier = Modifier
.fillMaxWidth()
.testTag(bottomNavigationContentDescription)
)
...
- 在
ReplyAppTest
类中,创建一个测试函数来测试紧凑尺寸屏幕。使用ReplyApp
可组合项设置composeTestRule
的内容,并将WindowWidthSizeClass.Compact
作为windowSize
参数传递。
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
}
- 断言底部导航元素是否存在并带有测试标签。在
composeTestRule
上调用扩展函数onNodeWithTagForStringId
,并传递导航底部字符串,然后调用assertExists()
方法。
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
// Bottom navigation is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_bottom
).assertExists()
}
- 运行测试并验证其是否通过。
为中等和扩展屏幕尺寸配置测试
既然您已成功为紧凑屏幕创建了测试,那么让我们为中等和扩展屏幕创建相应的测试。
要创建测试以验证中等和扩展屏幕中导航栏和永久导航抽屉的存在,请完成以下步骤
- 为导航栏定义一个字符串资源,以便稍后用作测试标签。
strings.xml
...
<resources>
...
<string name="navigation_rail">Navigation Rail</string>
...
</resources>
- 通过
PermanentNavigationDrawer
可组合项中的Modifier
将字符串作为测试标签传递。
ReplyHomeScreen.kt
...
val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
- 通过
ReplyNavigationRail
可组合项中的Modifier
将字符串作为测试标签传递。
ReplyHomeScreen.kt
...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
...
modifier = Modifier
.testTag(navigationRailContentDescription)
)
...
- 添加一个测试以验证中等屏幕中导航栏元素是否存在。
ReplyAppTest.kt
...
@Test
fun mediumDevice_verifyUsingNavigationRail() {
// Set up medium window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Medium
)
}
// Navigation rail is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_rail
).assertExists()
}
- 添加一个测试以验证扩展屏幕中导航抽屉元素是否存在。
ReplyAppTest.kt
...
@Test
fun expandedDevice_verifyUsingNavigationDrawer() {
// Set up expanded window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Expanded
)
}
// Navigation drawer is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_drawer
).assertExists()
}
- 使用平板电脑模拟器或平板电脑模式下的可调整大小的模拟器运行测试。
- 运行所有测试并验证它们是否通过。
测试紧凑屏幕中的配置更改
配置更改是在应用生命周期中发生的常见事件。例如,当您将方向从纵向更改为横向时,就会发生配置更改。当发生配置更改时,务必测试您的应用是否保留其状态。接下来,您将创建模拟配置更改的测试,以测试您的应用是否在紧凑屏幕中保留其状态。
要测试紧凑屏幕中的配置更改
- 在测试目录中,创建一个名为
ReplyAppStateRestorationTest.kt
的新 Kotlin 类。 - 在
ReplyAppStateRestorationTest
类中,使用createAndroidComposeRule
创建测试规则,并将ComponentActivity
作为类型参数传递。
ReplyAppStateRestorationTest.kt
...
class ReplyAppStateRestorationTest {
/**
* Note: To access to an empty activity, the code uses ComponentActivity instead of
* MainActivity.
*/
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
}
...
- 创建一个测试函数以验证配置更改后紧凑屏幕中是否仍选中电子邮件。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
要测试配置更改,您需要使用StateRestorationTester
。
- 通过将
composeTestRule
作为参数传递给StateRestorationTester
来设置stateRestorationTester
。 - 使用
setContent()
和ReplyApp
可组合项,并将WindowWidthSizeClass.Compact
作为windowSize
参数传递。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
}
...
- 验证应用中是否显示第三封电子邮件。在
composeTestRule
上使用assertIsDisplayed()
方法,该方法查找第三封电子邮件的文本。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
}
...
- 通过点击电子邮件主题导航到电子邮件详细信息屏幕。使用
performClick()
方法导航。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
}
...
- 验证详细信息屏幕中是否显示第三封电子邮件。断言后退按钮的存在以确认应用位于详细信息屏幕,并验证第三封电子邮件的文本是否显示。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
}
...
- 使用
stateRestorationTester.emulateSavedInstanceStateRestore()
模拟配置更改。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
}
...
- 再次验证详细信息屏幕中是否显示第三封电子邮件。断言后退按钮的存在以确认应用位于详细信息屏幕,并验证第三封电子邮件的文本是否显示。
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
// Verify that it still shows the detailed screen for the same email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
}
...
- 使用手机模拟器或手机模式下的可调整大小的模拟器运行测试。
- 验证测试是否通过。
测试扩展屏幕中的配置更改
要通过模拟配置更改并传递相应的 WindowWidthSizeClass 来测试扩展屏幕中的配置更改,请完成以下步骤
- 创建一个测试函数以验证配置更改后详细信息屏幕中是否仍选中电子邮件。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
要测试配置更改,您需要使用StateRestorationTester
。
- 通过将
composeTestRule
作为参数传递给StateRestorationTester
来设置stateRestorationTester
。 - 使用
setContent()
和ReplyApp
可组合项,并将WindowWidthSizeClass.Expanded
作为windowSize
参数传递。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
- 验证应用中是否显示第三封电子邮件。在
composeTestRule
上使用assertIsDisplayed()
方法,该方法查找第三封电子邮件的文本。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
}
...
- 在详细信息屏幕上选择第三封电子邮件。使用
performClick()
方法选择电子邮件。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
...
}
...
- 通过在详细信息屏幕上使用
testTag
并查找其子元素中的文本,验证详细信息屏幕是否显示第三封电子邮件。这种方法确保您可以在详细信息部分而不是电子邮件列表中找到文本。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
...
}
...
- 使用
stateRestorationTester.emulateSavedInstanceStateRestore()
模拟配置更改。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
...
}
...
- 再次验证配置更改后详细信息屏幕是否显示第三封电子邮件。
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
// Verify that third email is still displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
}
...
- 使用平板电脑模拟器或平板电脑模式下的可调整大小的模拟器运行测试。
- 验证测试是否通过。
使用注解对不同屏幕尺寸的测试进行分组
您可能会从之前的测试中意识到,某些测试在与不兼容的屏幕尺寸的设备上运行时会失败。虽然您可以使用合适的设备逐一运行测试,但当您有很多测试用例时,这种方法可能无法扩展。
为了解决此问题,您可以创建注解来表示测试可以在哪些屏幕尺寸上运行,并为相应的设备配置带注解的测试。
要根据屏幕尺寸运行测试,请完成以下步骤
- 在测试目录中,创建
TestAnnotations.kt
,其中包含三个注解类:TestCompactWidth
、TestMediumWidth
、TestExpandedWidth
。
TestAnnotations.kt
...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
- 通过在
ReplyAppTest
和ReplyAppStateRestorationTest
中紧凑测试的测试注解后放置TestCompactWidth
注解,在测试函数上使用这些注解进行紧凑测试。
ReplyAppTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_verifyUsingBottomNavigation() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
- 通过在
ReplyAppTest
中中等测试的测试注解后放置TestMediumWidth
注解,在测试函数上使用这些注解进行中等测试。
ReplyAppTest.kt
...
@Test
@TestMediumWidth
fun mediumDevice_verifyUsingNavigationRail() {
...
- 通过在
ReplyAppTest
和ReplyAppStateRestorationTest
中扩展测试的测试注解后放置TestExpandedWidth
注解,在测试函数上使用这些注解进行扩展测试。
ReplyAppTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_verifyUsingNavigationDrawer() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
为了确保成功,请将测试配置为仅运行用TestCompactWidth
注解的测试。
- 在 Android Studio 中,选择 **运行** > **编辑配置...**
- 将测试重命名为 **Compact tests**,并选择运行 **包中所有** 测试。
- 点击 **Instrumentation arguments** 字段右侧的三个点 (**...**)。
- 点击加号 (
+
) 按钮,并添加额外的参数:**annotation**,其值为 **com.example.reply.test.TestCompactWidth**。
- 使用紧凑型模拟器运行测试。
- 检查是否只运行了紧凑型测试。
- 对中型和扩展屏幕重复上述步骤。
6. 获取解决方案代码
要下载完成的 codelab 代码,请使用以下 git 命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git
或者,您可以将存储库下载为 zip 文件,解压缩并在 Android Studio 中打开它。
如果您想查看解决方案代码,请 在 GitHub 上查看。
7. 结论
恭喜!您通过实现自适应布局,使 Reply 应用能够适应所有屏幕尺寸。您还学习了如何使用预览加速开发,以及使用各种测试方法维护应用质量。
不要忘记在社交媒体上分享您的作品,并使用 #AndroidBasics 标签!