1. 开始之前
到目前为止,您所开发的应用都只有一个屏幕。但是,您使用的许多应用可能有多个屏幕,您可以在这些屏幕之间导航。例如,“设置”应用将大量内容分布在不同的屏幕上。
在现代 Android 开发中,多屏幕应用是使用 Jetpack Navigation 组件创建的。Navigation Compose 组件允许您使用声明式方法轻松地在 Compose 中构建多屏幕应用,就像构建用户界面一样。此代码实验室介绍了 Navigation Compose 组件的基本知识,如何使 AppBar 响应,以及如何使用 intent 将数据从您的应用发送到另一个应用,同时在一个越来越复杂的应用中演示最佳实践。
先决条件
- 熟悉 Kotlin 语言,包括函数类型、lambda 和作用域函数
- 熟悉 Compose 中的基本
Row
和Column
布局
您将学到什么
- 创建一个
NavHost
可组合项以定义应用中的路由和屏幕。 - 使用
NavHostController
在屏幕之间导航。 - 操作后退栈以导航到以前的屏幕。
- 使用 intent 与其他应用共享数据。
- 自定义 AppBar,包括标题和后退按钮。
您将构建什么
- 您将在多屏幕应用中实现导航。
您需要什么
- 最新版本的 Android Studio
- 下载初始代码的互联网连接
2. 下载初始代码
要开始,请下载初始代码
或者,您可以克隆代码的 GitHub 存储库
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git $ cd basic-android-kotlin-compose-training-cupcake $ git checkout starter
如果您想查看此代码实验室的初始代码,请在 GitHub 上查看。
3. 应用演练
Cupcake 应用与您之前使用的应用略有不同。应用不是将所有内容都显示在一个屏幕上,而是有四个单独的屏幕,用户可以在订购纸杯蛋糕时浏览每个屏幕。如果您运行该应用,您将看不到任何内容,也无法在这些屏幕之间导航,因为导航组件尚未添加到应用代码中。但是,您仍然可以检查每个屏幕的可组合项预览,并将它们与下面的最终应用屏幕进行匹配。
开始订单屏幕
第一个屏幕向用户显示三个按钮,对应于要订购的纸杯蛋糕数量。
在代码中,这由 StartOrderScreen.kt
中的 StartOrderScreen
可组合项表示。
该屏幕包含一个单列,其中包含图像和文本,以及三个自定义按钮以订购不同数量的纸杯蛋糕。自定义按钮由 SelectQuantityButton
可组合项实现,该可组合项也位于 StartOrderScreen.kt
中。
选择口味屏幕
选择数量后,应用会提示用户选择纸杯蛋糕口味。该应用使用所谓的单选按钮来显示不同的选项。用户可以选择多种口味中的其中一种。
可能的口味列表存储为 data.DataSource.kt
中的字符串资源 ID 列表。
选择取货日期屏幕
选择口味后,应用会向用户显示另一组单选按钮以选择取货日期。取货选项来自 OrderViewModel
中的 pickupOptions()
函数返回的列表。
选择口味屏幕和选择取货日期屏幕都由同一个可组合项 SelectOptionScreen.kt
中的 SelectOptionScreen
表示。为什么要使用同一个可组合项?这些屏幕的布局完全相同!唯一的区别是数据,但您可以使用同一个可组合项来显示口味和取货日期屏幕。
订单摘要屏幕
选择取货日期后,应用会显示订单摘要屏幕,用户可以在其中查看并完成订单。
此屏幕由 SummaryScreen.kt
中的 OrderSummaryScreen
可组合项实现。
布局包含一个 Column
,其中包含有关其订单的所有信息,一个用于小计的 Text
可组合项,以及用于将订单发送到另一个应用或取消订单并返回到第一个屏幕的按钮。
如果用户选择将订单发送到另一个应用,Cupcake 应用会显示一个 Android ShareSheet,其中显示不同的共享选项。
应用的当前状态存储在 data.OrderUiState.kt
中。OrderUiState
数据类包含用于存储用户从每个屏幕中选择的属性。
应用的屏幕将显示在 CupcakeApp
可组合项中。但是,在初始项目中,应用仅显示第一个屏幕。目前无法浏览应用的所有屏幕,但不用担心,这就是您来这里的目的!您将学习如何定义导航路由,设置 NavHost 可组合项以在屏幕(也称为目标)之间导航,执行 intent 以与系统 UI 组件(如共享屏幕)集成,以及使 AppBar 响应导航更改。
可重复使用的可组合项
在适当的情况下,本课程中的示例应用旨在实施最佳实践。Cupcake 应用也不例外。在ui.components 包中,您会看到一个名为 CommonUi.kt
的文件,其中包含 FormattedPriceLabel
可组合项。应用中的多个屏幕使用此可组合项以一致的方式格式化订单价格。与其重复使用具有相同格式和修饰符的相同 Text
可组合项,不如定义一次 FormattedPriceLabel
,然后根据需要在其他屏幕中重复使用它。
口味和取货日期屏幕使用 SelectOptionScreen
可组合项,该可组合项也可重复使用。此可组合项采用名为 options
的参数,其类型为 List<String>
,表示要显示的选项。选项显示在 Row
中,包含一个 RadioButton
可组合项和一个包含每个字符串的 Text
可组合项。Column
包含整个布局,还包含一个 Text
可组合项以显示格式化的价格、一个“取消”按钮和一个“下一步”按钮。
4. 定义路由并创建 NavHostController
导航组件的组成部分
导航组件主要包含三个部分
- NavController:负责在目标(即应用中的屏幕)之间导航。
- NavGraph:将可组合的目标映射到要导航到的位置。
- NavHost:充当容器,用于显示 NavGraph 的当前目标的可组合项。
在此代码实验室中,您将重点关注 NavController 和 NavHost。在 NavHost 中,您将为 Cupcake 应用的 NavGraph 定义目标。
为应用中的目标定义路由
Compose 应用中导航的基本概念之一是路由。路由是对应于目标的字符串。此概念类似于 URL 的概念。就像不同的 URL 映射到网站上的不同页面一样,路由是映射到目标并充当其唯一标识符的字符串。目标通常是单个可组合项或对应于用户所看到内容的可组合项组。Cupcake 应用需要开始订单屏幕、口味屏幕、取货日期屏幕和订单摘要屏幕的目标。
应用中的屏幕数量是有限的,因此路由的数量也是有限的。您可以使用枚举类定义应用的路由。Kotlin 中的枚举类具有 name 属性,该属性返回一个包含属性名称的字符串。
您将首先定义 Cupcake 应用的四个路由。
Start
:从三个按钮之一选择纸杯蛋糕数量。Flavor
:从选项列表中选择口味。Pickup
:从选项列表中选择取货日期。Summary
:查看所选内容,然后发送或取消订单。
添加枚举类以定义路由。
- 在
CupcakeScreen.kt
中,在CupcakeAppBar
可组合项上方,添加一个名为CupcakeScreen
的枚举类。
enum class CupcakeScreen() {
}
- 向枚举类添加四个情况:
Start
、Flavor
、Pickup
和Summary
。
enum class CupcakeScreen() {
Start,
Flavor,
Pickup,
Summary
}
向应用添加 NavHost
NavHost 是一个可组合项,根据给定的路由显示其他可组合的目标。例如,如果路由是 Flavor
,则 NavHost
将显示选择纸杯蛋糕口味的屏幕。如果路由是 Summary
,则应用会显示摘要屏幕。
NavHost
的语法与任何其他可组合项一样。
有两个值得注意的参数。
navController
:NavHostController
类的实例。您可以使用此对象在屏幕之间导航,例如,通过调用navigate()
方法导航到另一个目标。您可以通过从可组合函数中调用rememberNavController()
来获取NavHostController
。startDestination
:定义应用首次显示NavHost
时默认显示的目标的字符串路由。对于 Cupcake 应用,这应该是Start
路由。
与其他可组合项一样,NavHost
也采用 modifier
参数。
您将向 CupcakeScreen.kt
中的 CupcakeApp
可组合项添加 NavHost
。首先,您需要导航控制器的引用。您可以在现在添加的 NavHost
和以后步骤中添加的 AppBar
中使用导航控制器。因此,您应该在 CupcakeApp()
可组合项中声明该变量。
- 打开
CupcakeScreen.kt
。 - 在
Scaffold
内部,uiState
变量下方,添加一个NavHost
可组合项。
import androidx.navigation.compose.NavHost
Scaffold(
...
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost()
}
- 将
navController
变量传递给navController
参数,并将CupcakeScreen.Start.name
传递给startDestination
参数。将传递给CupcakeApp()
的修饰符传递给修饰符参数。将一个空的后置 lambda 传递给最后一个参数。
import androidx.compose.foundation.layout.padding
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
}
在你的 NavHost
中处理路由。
像其他可组合项一样,NavHost
为其内容采用函数类型。
在 NavHost
的内容函数内,你调用 composable()
函数。composable()
函数有两个必需参数。
route
:与路由名称相对应的字符串。这可以是任何唯一的字符串。你将使用CupcakeScreen
枚举常量的 name 属性。content
:在这里,你可以调用一个可组合项,你想为给定的路由显示该可组合项。
你将为所有四个路由各调用一次 composable()
函数。
- 调用
composable()
函数,将CupcakeScreen.Start.name
传递给route
。
import androidx.navigation.compose.composable
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- 在后置 lambda 中,调用
StartOrderScreen
可组合项,将quantityOptions
传递给quantityOptions
属性。对于modifier
,传递Modifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))
。
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
- 在第一次调用
composable()
下方,再次调用composable()
,将CupcakeScreen.Flavor.name
传递给route
。
composable(route = CupcakeScreen.Flavor.name) {
}
- 在后置 lambda 中,获取对
LocalContext.current
的引用,并将其存储在一个名为context
的变量中。Context
是一个抽象类,其实现由 Android 系统提供。它允许访问特定于应用程序的资源和类,以及用于应用程序级操作(如启动活动等)的上行调用。你可以使用此变量从视图模型中的资源 ID 列表中获取字符串,以显示口味列表。
import androidx.compose.ui.platform.LocalContext
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
}
- 调用
SelectOptionScreen
可组合项。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
)
}
- 口味屏幕需要在用户选择口味时显示和更新小计。将
uiState.price
传递给subtotal
参数。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- 口味屏幕从应用程序的字符串资源中获取口味列表。使用
map()
函数并将每个口味的context.resources.getString(id)
进行转换,将资源 ID 列表转换为字符串列表。
import com.example.cupcake.ui.SelectOptionScreen
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) }
)
}
- 对于
onSelectionChanged
参数,传递一个 lambda 表达式,该表达式在视图模型上调用setFlavor()
,并将it
(传递给onSelectionChanged()
的参数)传递给它。对于modifier
参数,传递Modifier.fillMaxHeight().
。
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
取货日期屏幕与口味屏幕类似。唯一的区别是传递给 SelectOptionScreen
可组合项的数据。
- 再次调用
composable()
函数,将CupcakeScreen.Pickup.name
传递给route
参数。
composable(route = CupcakeScreen.Pickup.name) {
}
- 在后置 lambda 中,调用
SelectOptionScreen
可组合项,并将uiState.price
传递给subtotal
(如前所述)。将uiState.pickupOptions
传递给options
参数,并将调用viewModel
上的setDate()
的 lambda 表达式传递给onSelectionChanged
参数。对于modifier
参数,传递Modifier.fillMaxHeight().
。
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- 再调用一次
composable()
,将CupcakeScreen.Summary.name
传递给route
。
composable(route = CupcakeScreen.Summary.name) {
}
- 在后置 lambda 中,调用
OrderSummaryScreen()
可组合项,将uiState
变量传递给orderUiState
参数。对于modifier
参数,传递Modifier.fillMaxHeight().
。
import com.example.cupcake.ui.OrderSummaryScreen
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
modifier = Modifier.fillMaxHeight()
)
}
设置 NavHost
就完成了。在下一节中,你将使你的应用程序更改路由并在用户点击每个按钮时在屏幕之间导航。
5. 在路由之间导航
既然你已经定义了你的路由并在 NavHost
中将其映射到可组合项,那么现在该在屏幕之间导航了。NavHostController
(来自调用 rememberNavController()
的 navController
属性)负责在路由之间导航。但是,请注意,此属性是在 CupcakeApp
可组合项中定义的。你需要一种方法来从应用程序中的不同屏幕访问它。
很简单,对吧?只需将 navController
作为参数传递给每个可组合项。
虽然这种方法有效,但它并不是构建应用程序的理想方法。使用 NavHost 处理应用程序导航的一个好处是,导航逻辑**与单个 UI 分开保持**。此选项避免了将 navController
作为参数传递的一些主要缺点。
- 导航逻辑集中在一个地方,这可以使你的代码更易于维护,并通过避免意外地赋予各个屏幕在你的应用程序中随意导航的权限来防止错误。
- 在需要在不同表单因子(如纵向模式手机、折叠手机或大屏幕平板电脑)上工作的应用程序中,按钮可能会也可能不会触发导航,具体取决于应用程序的布局。各个屏幕应该是独立的,并且不需要了解应用程序中的其他屏幕。
相反,我们的方法是将函数类型传递给每个可组合项,以确定用户点击按钮时应该发生什么。这样,可组合项及其任何子可组合项都可以决定何时调用该函数。但是,导航逻辑不会暴露给应用程序中的各个屏幕。所有导航行为都在 NavHost 中处理。
向 StartOrderScreen
添加按钮处理程序
你将首先添加一个函数类型参数,当在第一个屏幕上按下数量按钮之一时,该参数会被调用。此函数传递给 StartOrderScreen
可组合项,并负责更新视图模型并导航到下一个屏幕。
- 打开
StartOrderScreen.kt
。 - 在
quantityOptions
参数下方,以及修饰符参数之前,添加一个名为onNextButtonClicked
的参数,其类型为() -> Unit
。
@Composable
fun StartOrderScreen(
quantityOptions: List<Pair<Int, Int>>,
onNextButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
- 现在
StartOrderScreen
可组合项期望onNextButtonClicked
的值,找到StartOrderPreview
并将一个空 lambda 主体传递给onNextButtonClicked
参数。
@Preview
@Composable
fun StartOrderPreview() {
CupcakeTheme {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
每个按钮对应于不同数量的纸杯蛋糕。你需要此信息,以便为 onNextButtonClicked
传递的函数可以相应地更新视图模型。
- 将
onNextButtonClicked
参数的类型修改为接受一个Int
参数。
onNextButtonClicked: (Int) -> Unit,
要获取在调用 onNextButtonClicked()
时传递的 Int
,请查看 quantityOptions
参数的类型。
类型为 List<Pair<Int, Int>>
或 Pair<Int, Int>
的列表。Pair
类型可能对你来说不熟悉,但正如其名称所示,它只是一对值。Pair
接受两个泛型类型参数。在本例中,它们都是 Int
类型。
每对中的每个项目都通过 first 属性或 second 属性访问。在 StartOrderScreen
可组合项的 quantityOptions
参数的情况下,第一个 Int
是要显示在每个按钮上的字符串的资源 ID。第二个 Int
是纸杯蛋糕的实际数量。
在调用 onNextButtonClicked()
函数时,我们将传递所选对的 second 属性。
- 找到
SelectQuantityButton
的onClick
参数的空 lambda 表达式。
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = {}
)
}
- 在 lambda 表达式中,调用
onNextButtonClicked
,并将item.second
(纸杯蛋糕的数量)传递给它。
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { onNextButtonClicked(item.second) }
)
}
向 SelectOptionScreen 添加按钮处理程序
- 在
SelectOptionScreen.kt
中SelectOptionScreen
可组合项的onSelectionChanged
参数下方,添加一个名为onCancelButtonClicked
的参数,其类型为() -> Unit
,默认值为{}
。
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- 在
onCancelButtonClicked
参数下方,添加另一个类型为() -> Unit
的参数,命名为onNextButtonClicked
,默认值为{}
。
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- 将
onCancelButtonClicked
传递给取消按钮的onClick
参数。
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
- 将
onNextButtonClicked
传递给下一个按钮的onClick
参数。
Button(
modifier = Modifier.weight(1f),
enabled = selectedValue.isNotEmpty(),
onClick = onNextButtonClicked
) {
Text(stringResource(R.string.next))
}
向 SummaryScreen 添加按钮处理程序
最后,为摘要屏幕上的“取消”和“发送”按钮添加按钮处理程序函数。
- 在
SummaryScreen.kt
中的OrderSummaryScreen
可组合项中,添加一个名为onCancelButtonClicked
的参数,其类型为() -> Unit
。
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
- 添加另一个类型为
(String, String) -> Unit
的参数,并将其命名为onSendButtonClicked
。
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- 现在
OrderSummaryScreen
可组合项期望onSendButtonClicked
和onCancelButtonClicked
的值。找到OrderSummaryPreview
,将带有两个String
参数的空 lambda 主体传递给onSendButtonClicked
,并将空 lambda 主体传递给onCancelButtonClicked
参数。
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
- 将
onSendButtonClicked
传递给“发送”按钮的onClick
参数。传递newOrder
和orderSummary
,这两个变量在OrderSummaryScreen
中前面定义过。这些字符串包含用户可以与其他应用程序共享的实际数据。
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
Text(stringResource(R.string.send))
}
- 将
onCancelButtonClicked
传递给“取消”按钮的onClick
参数。
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
导航到另一个路由
要导航到另一个路由,只需在 NavHostController
的实例上调用 navigate()
方法。
navigate 方法接受一个参数:一个 String
,对应于在 NavHost
中定义的路由。如果该路由与 NavHost
中对 composable()
的调用之一匹配,则应用程序会导航到该屏幕。
您将传入一些函数,当用户按下 Start
、Flavor
和 Pickup
屏幕上的按钮时,这些函数会调用 navigate()
。
- 在
CupcakeScreen.kt
中,找到对启动屏幕的composable()
的调用。对于onNextButtonClicked
参数,传入一个 lambda 表达式。
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
}
)
还记得传递给此函数的用于表示纸杯蛋糕数量的 Int
属性吗?在导航到下一个屏幕之前,您应该更新视图模型,以便应用程序显示正确的小计。
- 在
viewModel
上调用setQuantity
,并将it
传入。
onNextButtonClicked = {
viewModel.setQuantity(it)
}
- 在
navController
上调用navigate()
,将CupcakeScreen.Flavor.name
作为route
传入。
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- 对于口味屏幕上的
onNextButtonClicked
参数,只需传入一个调用navigate()
的 lambda,并将CupcakeScreen.Pickup.name
作为route
传入。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 为
onCancelButtonClicked
传入一个空 lambda,您将在后面实现它。
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = {},
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
- 对于提货屏幕上的
onNextButtonClicked
参数,传入一个调用navigate()
的 lambda,并将CupcakeScreen.Summary.name
作为route
传入。
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 同样,为
onCancelButtonClicked()
传入一个空 lambda。
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = {},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- 对于
OrderSummaryScreen
,为onCancelButtonClicked
和onSendButtonClicked
传入空 lambda。添加subject
和summary
参数,这些参数将传递到onSendButtonClicked
中,您将很快实现它们。
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
您现在应该能够浏览应用程序的每个屏幕。请注意,通过调用 navigate()
,不仅屏幕会发生变化,而且实际上它还会放置在返回堆栈的顶部。此外,当您按下系统后退按钮时,您可以导航回上一个屏幕。
应用程序将每个屏幕堆叠在上一个屏幕的顶部,后退按钮()可以移除它们。从底部 startDestination
到刚刚显示的最顶层屏幕的屏幕历史称为返回堆栈。
返回到启动屏幕
与系统后退按钮不同,取消按钮不会返回到上一个屏幕。相反,它应该弹出(删除)返回堆栈中的所有屏幕并返回到起始屏幕。
您可以通过调用 popBackStack()
方法来实现。
popBackStack()
方法有两个必需的参数。
route
:表示要导航回的目标的路由的字符串。inclusive
:一个布尔值,如果为 true,则还会弹出(删除)指定的路由。如果为 false,popBackStack()
将删除起始目标之上(但不包括起始目标)的所有目标,使其成为用户可见的最顶层屏幕。
当用户按下任何屏幕上的取消按钮时,应用程序会重置视图模型中的状态并调用 popBackStack()
。您将首先实现一个执行此操作的方法,然后将其作为所有三个带有取消按钮的屏幕上的适当参数传入。
- 在
CupcakeApp()
函数之后,定义一个名为cancelOrderAndNavigateToStart()
的私有函数。
private fun cancelOrderAndNavigateToStart() {
}
- 添加两个参数:类型为
OrderViewModel
的viewModel
和类型为NavHostController
的navController
。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
}
- 在函数体中,在
viewModel
上调用resetOrder()
。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
}
- 在
navController
上调用popBackStack()
,将CupcakeScreen.Start.name
作为route
传入,将false
作为inclusive
传入。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
- 在
CupcakeApp()
可组合项中,将cancelOrderAndNavigateToStart
作为两个SelectOptionScreen
可组合项和OrderSummaryScreen
可组合项的onCancelButtonClicked
参数传入。
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
- 运行您的应用程序并测试按下任何屏幕上的取消按钮是否会将用户导航回第一个屏幕。
6. 导航到另一个应用程序
到目前为止,您已经学习了如何在应用程序中导航到不同的屏幕以及如何导航回主屏幕。在 Cupcake 应用程序中实现导航,只剩下一最后一步。在订单摘要屏幕上,用户可以将其订单发送到另一个应用程序。此选择会调出一个 ShareSheet(一个覆盖屏幕底部部分的用户界面组件),其中显示共享选项。
此 UI 部分不属于 Cupcake 应用程序。实际上,它是 Android 操作系统提供的。系统 UI(例如共享屏幕)不会由您的 navController
调用。相反,您使用的是称为Intent的东西。
Intent 是对系统执行某些操作(通常是呈现新活动)的请求。有许多不同的 Intent,建议您参考文档以获取完整列表。但是,我们对名为 ACTION_SEND
的 Intent 感兴趣。您可以为此 Intent 提供一些数据(例如字符串),并为该数据呈现适当的共享操作。
设置 Intent 的基本流程如下
- 创建一个 Intent 对象并指定 Intent,例如
ACTION_SEND
。 - 指定与 Intent 一起发送的其他数据的类型。对于简单的文本,您可以使用
"text/plain"
,尽管还有其他类型可用,例如"image/*"
或"video/*"
。 - 通过调用
putExtra()
方法将任何其他数据(例如要共享的文本或图像)传递给 Intent。此 Intent 将接收两个额外参数:EXTRA_SUBJECT
和EXTRA_TEXT
。 - 调用上下文的
startActivity()
方法,传入从 Intent 创建的活动。
我们将引导您完成如何创建共享操作 Intent 的过程,但对于其他类型的 Intent,过程相同。对于未来的项目,建议您根据需要参考文档以了解特定类型的数据和必要的额外参数。
完成以下步骤以创建将纸杯蛋糕订单发送到另一个应用程序的 Intent
- 在 **CupcakeScreen.kt** 中,在
CupcakeApp
可组合项下方,创建一个名为shareOrder()
的私有函数。
private fun shareOrder()
- 添加一个名为
context
的参数,其类型为Context
。
import android.content.Context
private fun shareOrder(context: Context) {
}
- 添加两个
String
参数:subject
和summary
。这些字符串将显示在共享操作表上。
private fun shareOrder(context: Context, subject: String, summary: String) {
}
- 在函数体中,创建一个名为
intent
的 Intent,并将Intent.ACTION_SEND
作为参数传入。
import android.content.Intent
val intent = Intent(Intent.ACTION_SEND)
由于您只需要配置一次此 Intent
对象,因此您可以使用在之前的 codelab 中学习的 apply()
函数使接下来的几行代码更简洁。
- 在新创建的 Intent 上调用
apply()
并传入一个 lambda 表达式。
val intent = Intent(Intent.ACTION_SEND).apply {
}
- 在 lambda 体中,将类型设置为
"text/plain"
。因为您在传递给apply()
的函数中执行此操作,所以您不需要引用对象的标识符intent
。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
- 调用
putExtra()
,将subject
作为EXTRA_SUBJECT
传入。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
- 调用
putExtra()
,将summary
作为EXTRA_TEXT
传入。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
- 调用上下文的
startActivity()
方法。
context.startActivity(
)
- 在传递给
startActivity()
的 lambda 中,通过调用类方法createChooser()
从 Intent 创建一个活动。将 intent 作为第一个参数传入,并将new_cupcake_order
字符串资源作为第二个参数传入。
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- 在
CupcakeApp
可组合项中,在对composable()
的调用(用于CucpakeScreen.Summary.name
)中,获取对 context 对象的引用,以便您可以将其传递给shareOrder()
函数。
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
...
}
- 在
onSendButtonClicked()
的 lambda 体中,调用shareOrder()
,并将context
、subject
和summary
作为参数传入。
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
}
- 运行您的应用程序并浏览屏幕。
当您点击将订单发送到另一个应用程序时,您应该在底部工作表上看到共享操作(例如消息和蓝牙),以及您作为额外参数提供的主题和摘要。
7. 使应用栏响应导航
即使您的应用程序可以正常运行并且可以导航到和离开每个屏幕,但在本 codelab 开头的屏幕截图中仍然缺少某些内容。应用栏不会自动响应导航。当应用程序导航到新路由时,标题不会更新,并且在适当情况下也不会在标题之前显示向上按钮。
启动代码包含一个用于管理名为CupcakeAppBar
的AppBar
的可组合函数。现在您已在应用中实现了导航,您可以使用来自返回栈的信息来显示正确的标题,并在适当的情况下显示“向上”按钮。CupcakeAppBar
可组合函数应该知道当前屏幕,以便标题能够相应地更新。
- 在**CupcakeScreen.kt**中的
CupcakeScreen
枚举中,使用@StringRes
注解添加一个名为title
、类型为Int
的参数。
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- 为每个枚举情况添加一个资源值,对应于每个屏幕的标题文本。对于
Start
屏幕使用app_name
,对于Flavor
屏幕使用choose_flavor
,对于Pickup
屏幕使用choose_pickup_date
,对于Summary
屏幕使用order_summary
。
enum class CupcakeScreen(@StringRes val title: Int) {
Start(title = R.string.app_name),
Flavor(title = R.string.choose_flavor),
Pickup(title = R.string.choose_pickup_date),
Summary(title = R.string.order_summary)
}
- 向
CupcakeAppBar
可组合函数添加一个名为currentScreen
、类型为CupcakeScreen
的参数。
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
)
- 在
CupcakeAppBar
内部,将硬编码的应用名称替换为当前屏幕的标题,方法是将currentScreen.title
传递给TopAppBar
标题参数的stringResource()
调用。
TopAppBar(
title = { Text(stringResource(currentScreen.title)) },
modifier = modifier,
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
}
)
只有当返回栈上存在可组合函数时,“向上”按钮才会显示。如果应用在返回栈上没有屏幕(显示StartOrderScreen
),则“向上”按钮不应显示。要检查这一点,您需要一个对返回栈的引用。
- 在
CupcakeApp
可组合函数中,在navController
变量下方,创建一个名为backStackEntry
的变量,并使用by
委托调用navController
的currentBackStackEntryAsState()
方法。
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
navController: NavHostController = rememberNavController()
){
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- 将当前屏幕的标题转换为
CupcakeScreen
的值。在backStackEntry
变量下方,使用val
创建一个名为currentScreen
的变量,使其等于调用CupcakeScreen
的valueOf()
类函数的结果,并将backStackEntry
目标的路由传递给它。使用Elvis运算符提供CupcakeScreen.Start.name
的默认值。
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
- 将
currentScreen
变量的值传递到CupcakeAppBar
可组合函数的同名参数中。
CupcakeAppBar(
currentScreen = currentScreen,
canNavigateBack = false,
navigateUp = {}
)
只要返回栈上存在当前屏幕后面的屏幕,“向上”按钮就会显示。您可以使用布尔表达式来识别是否应该显示“向上”按钮。
- 对于
canNavigateBack
参数,传递一个布尔表达式,检查navController
的previousBackStackEntry
属性是否不等于null。
canNavigateBack = navController.previousBackStackEntry != null,
- 要实际导航回上一屏,请调用
navController
的navigateUp()
方法。
navigateUp = { navController.navigateUp() }
- 运行您的应用。
请注意,AppBar
标题现在会更新以反映当前屏幕。当您导航到除StartOrderScreen
以外的屏幕时,“向上”按钮应该出现,并带您返回上一屏。
8. 获取解决方案代码
要下载完成的Codelab的代码,您可以使用以下git命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git $ cd basic-android-kotlin-compose-training-cupcake $ git checkout navigation
或者,您可以将存储库下载为zip文件,解压缩并将其在Android Studio中打开。
如果您想查看此Codelab的解决方案代码,请在GitHub上查看。
9. 总结
恭喜!您刚刚完成了从简单的单屏应用程序到使用Jetpack Navigation组件遍历多个屏幕的复杂多屏应用程序的转变。您定义了路由,在NavHost中处理它们,并使用函数类型参数将导航逻辑与各个屏幕分离。您还学习了如何使用意图将数据发送到另一个应用程序,以及如何根据导航自定义应用栏。在接下来的单元中,您将继续使用这些技能,因为您将在几个其他复杂程度不断提高的多屏应用程序上工作。