1. 准备工作
到目前为止,您创建的应用仅包含一个屏幕。不过,您使用的许多应用可能都有多个屏幕,您可以在这些屏幕之间导航。例如,Settings 应用中有多页内容分布在不同的界面上。
现代 Android 开发中,使用 Jetpack Navigation 组件创建多界面应用。借助 Navigation Compose 组件,您可以使用声明性方法在 Compose 中轻松构建多屏幕应用,就像构建界面一样。此 Codelab 介绍了 Navigation Compose 组件的基础知识、如何使应用栏具备自适应能力,以及如何使用 intent 将数据从您的应用发送到另一个应用,同时还将演示在日益复杂的应用中使用的最佳实践。
前提条件
- 熟悉 Kotlin 语言,包括函数类型、lambda 和作用域函数
- 熟悉 Compose 中的基本
Row
和Column
布局
学习内容
- 创建
NavHost
可组合项以定义应用中的路线和屏幕。 - 使用
NavHostController
在屏幕之间导航。 - 操控返回堆栈,以切换到之前的屏幕。
- 使用 intent 与其他应用共享数据。
- 自定义应用栏,包括标题和返回按钮。
构建内容
- 您将在多屏幕应用中实现导航功能。
所需条件
- 最新版本的 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
如果您想查看此 Codelab 的起始代码,请前往 GitHub 查看。
3. 应用演示
Cupcake 应用与您到目前为止所使用过的应用略有不同。该应用不是在单个屏幕上显示所有内容,而是采用四个单独的屏幕,并且用户可以在订购纸杯蛋糕时在各个屏幕之间切换。如果您运行应用,您将无法查看任何内容,也无法在这些屏幕之间进行导航,因为 navigation 组件尚未添加到应用代码中。不过,您仍可以检查每个屏幕的可组合项预览,并将它们与下方的最终应用屏幕配对。
Start Order 屏幕
第一个屏幕向用户显示三个按钮,这些按钮对应于要订购的纸杯蛋糕数量。
在代码中,这由 StartOrderScreen.kt
中的 StartOrderScreen
可组合项表示。
该屏幕包含一个列(包含图片和文字)以及三个用于订购不同数量的纸杯蛋糕的自定义按钮。自定义按钮由同样位于 StartOrderScreen.kt
中的 SelectQuantityButton
可组合项实现。
Choose Flavor 屏幕
选择数量后,应用会提示用户选择纸杯蛋糕的口味。应用使用单选按钮来显示不同的选项。用户可以从多种可选口味中选择一种口味。
可选口味列表以字符串资源 ID 列表的形式存储在 data.DataSource.kt
中。
Choose Pickup Date 屏幕
用户选择口味后,应用会向用户显示另一些单选按钮,用于选择自提日期。自提选项来自 OrderViewModel
中的 pickupOptions()
函数返回的列表。
Choose Flavor 屏幕和 Choose Pick-Date 屏幕均由 SelectOptionScreen.kt
中的相同可组合项 SelectOptionScreen
表示。为什么要使用相同的可组合项?因为这些屏幕的布局完全相同!唯一的区别在于数据,但您可以使用相同的可组合项来同时显示口味屏幕和自提日期屏幕。
Order Summary 屏幕
用户选择自提日期后,应用会显示 Order Summary 屏幕,用户可以在其中检查和完成订单。
此屏幕由 SummaryScreen.kt
中的 OrderSummaryScreen
可组合项实现。
布局包含一个 Column
(包含订单的所有信息)、一个 Text
可组合项(用于显示小计),以及用于将订单发送到其他应用或取消订单并返回第一个屏幕的多个按钮。
如果用户选择将订单发送到其他应用,Cupcake 应用会显示 Android ShareSheet,其中显示了不同的分享选项。
应用的当前状态存储在 data.OrderUiState.kt
中。OrderUiState
数据类包含用于存储在每个屏幕中为用户提供的可用选项的属性。
应用的屏幕将显示在 CupcakeApp
可组合项中。不过,在起始项目中,应用仅显示第一个屏幕。目前还无法在应用的所有屏幕之间导航,不过别担心,本课程的目的就是实现这种导航!您将学习如何定义导航路线,设置用于在屏幕(也称为目标页面)之间导航的 NavHost 可组合项、执行 intent 以与共享屏幕等系统界面组件集成,并让应用栏能够响应导航更改。
可重复使用的可组合项
在适当的情况下,本课程中的示例应用可实现最佳实践。Cupcake 应用也不例外。在 ui.components 软件包中,您会看到一个名为 CommonUi.kt
的文件,其中包含一个 FormattedPriceLabel
可组合项。应用中的多个屏幕使用此可组合项来统一设置订单价格的格式。您无需重复定义具有相同格式和修饰符的相同 Text
可组合项,而是只需定义一次 FormattedPriceLabel
,然后根据需要将其重复用于其他屏幕。
同样,口味屏幕和自提日期屏幕也使用可重复使用的 SelectOptionScreen
可组合项。此可组合项接受名为 options
且类型为 List<String>
的参数,该参数表示要显示的选项。这些选项显示在 Row
中,后者由一个 RadioButton
可组合项和一个包含各个字符串的 Text
可组合项组成。整个布局周围有一个 Column
,还包含一个用于显示格式化价格的 Text
可组合项、一个 Cancel 按钮和一个 Next 按钮。
4. 定义路线并创建 NavHostController
导航组件的组成部分
Navigation 组件有三个主要部分:
- NavController:负责在目标页面(即应用中的屏幕)之间导航。
- NavGraph:用于映射要导航到的可组合项目标页面。
- NavHost:此可组合项充当容器,用于显示 NavGraph 的当前目标页面。
在此 Codelab 中,您将重点关注 NavController 和 NavHost。在 NavHost 中,您将定义 Cupcake 应用的 NavGraph 的目标页面。
在应用中为目标页面定义路线
在 Compose 应用中,导航的一个基本概念就是路线。路线是与目标页面对应的字符串。这类似于网址的概念。就像不同网址映射到网站上的不同页面一样,路线是可映射至目标页面并作为其唯一标识符的字符串。目标页面通常是与用户看到的内容相对应的单个可组合项或一组可组合项。Cupcake 应用需要显示“Start Order”屏幕、“Flavor”屏幕、“Pickup Date”屏幕和“Order Summary”屏幕的目标页面。
应用中的屏幕数量有限,因此路线数量也是有限的。您可以使用枚举类来定义应用的路线。Kotlin 中的枚举类具有一个名称属性,该属性会返回具有属性名称的字符串。
首先,定义 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
变量,并为startDestination
参数传入CupcakeScreen.Start.name
。传递之前传入到修饰符参数的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
枚举的常量的名称属性。content
:您可以在此处调用要为特定路线显示的可组合项。
您将针对这四个路线分别调用一次 composable()
函数。
- 调用
composable()
函数,为route
传入CupcakeScreen.Start.name
。
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.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))
中的modifier
卡券
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()
,为route
传入CupcakeScreen.Flavor.name
。
composable(route = CupcakeScreen.Flavor.name) {
}
- 在尾随 lambda 中,获取对
LocalContext.current
的引用,并将其存储到名称为context
的变量中。Context
是一个抽象类,其实现由 Android 系统提供。它允许访问应用专用资源和类,以及向上调用应用级别的操作(例如启动 activity 等)。您可以使用此变量从视图模型中的资源 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(
)
}
- 当用户选择口味时,Flavor 屏幕需要显示和更新小计。为
subtotal
参数传入uiState.price
。
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- Flavor 屏幕从应用的字符串资源中获取口味列表。使用
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
参数,请传入一个对视图模型调用setFlavor()
的 lambda 表达式,并传入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()
函数,为route
参数传入CupcakeScreen.Pickup.name
。
composable(route = CupcakeScreen.Pickup.name) {
}
- 在尾随 lambda 中,调用
SelectOptionScreen
可组合项,并像之前一样为subtotal
传入uiState.price
。为options
参数传入uiState.pickupOptions
,并为onSelectionChanged
参数传入对viewModel
调用setDate()
的 lambda 表达式。 对于modifier
参数,传入Modifier.fillMaxHeight().
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- 再次调用
composable()
,并为route
传入CupcakeScreen.Summary.name
。
composable(route = CupcakeScreen.Summary.name) {
}
- 在跟随的 lambda 中,调用
OrderSummaryScreen()
可组合函数,并为orderUiState
形参传入uiState
变量。对于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 处理应用导航的一项优势就是,导航逻辑将各个界面相隔离。此方法可避免将 navController
作为参数传递时的一些主要缺点。
- 导航逻辑会保存在一个集中位置,这样可以避免意外允许各个屏幕在应用中随意导航,从而让您的代码更易于维护并预防 bug。
- 在需要处理不同外形规格(例如竖屏模式手机、可折叠手机或大屏平板电脑)的应用中,按钮可能会触发导航,也可能不会触发导航,具体取决于应用的布局。各个屏幕应保持独立,无需了解应用中其他屏幕的信息。
而我们的方法是为每个可组合项传入一个函数类型,以便确定在用户点击该按钮时应当发生什么。这样,可组合项及其任何子可组合项就可以确定何时调用函数。不过,导航逻辑不会向应用中的各个屏幕公开。所有导航行为都在 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
类型。
Pair 对中的每一项均由第一个属性或第二个属性访问。对于 StartOrderScreen
可组合项的 quantityOptions
参数,第一个 Int
是每个按钮上显示的字符串的资源 ID。第二个 Int
是纸杯蛋糕的实际数量。
当调用 onNextButtonClicked()
函数时,我们会传递所选 Pair 对的第二个属性。
- 为
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
)
- 为 Cancel 按钮的
onClick
参数传入onCancelButtonClicked
。
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
- 为 Next 按钮的
onClick
参数传入onNextButtonClicked
。
Button(
modifier = Modifier.weight(1f),
enabled = selectedValue.isNotEmpty(),
onClick = onNextButtonClicked
) {
Text(stringResource(R.string.next))
}
为 SummaryScreen 添加按钮处理程序
最后,为 Summary 屏幕上的 Cancel 和 Send 按钮添加按钮处理程序函数。
- 在
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()
)
}
}
- 为 Send 按钮的
onClick
参数传递onSendButtonClicked
。传入newOrder
和orderSummary
,这是之前在OrderSummaryScreen
中定义的两个变量。这些字符串由用户可以与其他应用共享的实际数据组成。
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
Text(stringResource(R.string.send))
}
- 为 Cancel 按钮的
onClick
参数传递onCancelButtonClicked
。
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
导航到其他路线
如需导航到其他路线,只需在 NavHostController
实例上调用 navigate()
方法即可。
navigation 方法仅接受一个参数:与 NavHost
中定义的路线相对应的 String
。如果路线与 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()
,并传入route
的CupcakeScreen.Flavor.name
。
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- 对于 Flavor 屏幕上的
onNextButtonClicked
参数,只需传入调用navigate()
的 lambda,并为route
传入CupcakeScreen.Pickup.name
。
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,您接下来要实现该 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()
)
- 对于 Pickup 屏幕上的
onNextButtonClicked
参数,请传入调用navigate()
的 lambda,并为route
传入CupcakeScreen.Summary.name
。
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。为传入到onSendButtonClicked
中的subject
和summary
添加参数,您很快就将实现这些参数。
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
您现在应当能够在应用的各个屏幕之间导航。请注意,通过调用 navigate()
,屏幕不仅会发生变化,而且会实际放置在返回堆栈之上。此外,当您点按系统返回按钮时,即可返回到上一个界面。
应用会将每个界面堆叠在上一个界面上,而返回按钮 () 可以移除这些界面。从底部 startDestination
到刚才显示的最顶部的屏幕的历史记录称为返回堆栈。
跳转至起始屏幕
与系统返回按钮不同,Cancel 按钮不会返回上一个屏幕。而是跳转移除返回堆栈中的所有屏幕,并返回起始屏幕。
您可以通过调用 popBackStack()
方法来实现此目的。
popBackStack()
方法有两个必需参数。
route
:此字符串表示您希望返回到的目标页面的路线。inclusive
:这是一个布尔值,如果为 true,还会弹出(移除)指定路线。如果为 false,popBackStack()
将移除起始目标页面之上的所有目标页面,但不包含该起始目标页面,并仅留下该起始目标页面作为最顶层的屏幕显示给用户。
当用户在任何屏幕上点按 Cancel 按钮时,应用会重置视图模型中的状态并调用 popBackStack()
。首先,您将实现一个方法来执行此操作,然后为包含 Cancel 按钮的所有三个屏幕的相应参数传入该方法。
- 在
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()
,为route
传入CupcakeScreen.Start.name
,并为inclusive
传入false
。
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
- 在
CupcakeApp()
可组合项中,为两个SelectOptionScreen
可组合项和OrderSummaryScreen
可组合项的onCancelButtonClicked
参数传入cancelOrderAndNavigateToStart
。
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()
)
}
- 运行应用,并测试点按任何屏幕上的 Cancel 按钮是否会返回第一个屏幕。
6. 导航到其他应用
到目前为止,您已经学习了如何导航到应用中的不同屏幕,以及如何返回主屏幕。在 Cupcake 应用中实现导航只剩最后一步了。在订单摘要屏幕上,用户可以将其订单发送到其他应用。此选项会打开一个 ShareSheet(覆盖屏幕底部部分的界面组件),其中会显示分享选项。
此部分界面不属于 Cupcake 应用。事实上,它是由 Android 操作系统提供的。您的 navController
不会调用系统界面(例如分享屏幕)。作为替代方案,您可以使用 intent。
intent 将请求系统执行某项操作,通常用于呈现新的 activity。有许多不同的 intent,建议您参阅相关文档,查看完整列表。不过,我们感兴趣的是 ACTION_SEND
。您可以向此 intent 提供某些数据(例如字符串),并为这些数据提供适当的分享操作。
设置 intent 的基本过程如下:
- 创建一个 intent 对象并指定 intent,例如
ACTION_SEND
。 - 指定随 intent 一同发送的其他数据类型。对于简单的一段文本,您可以使用
"text/plain"
,但也可以使用其他类型,例如"image/*"
或"video/*"
。 - 通过调用
putExtra()
方法,向 intent 传递任何其他数据,例如要分享的文本或图片。此 intent 将接受两个 extra:EXTRA_SUBJECT
和EXTRA_TEXT
。 - 调用上下文的
startActivity()
方法,并传入从 intent 创建的 activity。
我们将向您介绍如何创建分享操作 intent,但对于其他类型的 intent,流程是相同的。在后续项目中,建议您参阅特定数据类型和所需 extra 的相关文档。
如需创建 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()
,并传入EXTRA_SUBJECT
的 subject。
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
- 调用
putExtra()
,并传入EXTRA_TEXT
的 summary。
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 中创建一个 activity。为第一个参数和new_cupcake_order
字符串资源传递 intent。
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- 在
CupcakeApp
可组合项的CucpakeScreen.Summary.name
的composable()
调用中,获取对上下文对象的引用,以便将其传递给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)
}
- 运行应用并在各个屏幕间导航。
点击 Send Order to Other App 时,您应当会看到底部动作条上的分享操作(例如 Messaging 和 Bluetooth)以及您以 extra 形式提供的主题和摘要。
7. 让应用栏响应导航
尽管您的应用可以正常运行,并且可以在各个屏幕之间导航,但在此 Codelab 最开始的屏幕截图中仍缺少一些内容。应用栏无法自动响应导航。当应用导航至新路线时,不会更新标题,也不会适时在标题前显示向上按钮。
起始代码包含一个可组合项,用于管理名称为 CupcakeAppBar
的 AppBar
。现在,您已经在应用中实现了导航,接下来可以使用返回堆栈中的信息来显示正确的标题,并适时显示向上按钮。CupcakeAppBar
可组合项应了解当前屏幕,以便标题进行相应更新。
- 在 CupcakeScreen.kt 中的
CupcakeScreen
枚举中,使用@StringRes
注解添加类型为Int
且名为title
的参数。
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- 为每个枚举用例添加资源值,与各屏幕的标题文本相对应。将
app_name
用于Start
屏幕,choose_flavor
用于Flavor
屏幕,choose_pickup_date
用于Pickup
屏幕,order_summary
用于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)
}
- 将名为
currentScreen
且类型为CupcakeScreen
的参数添加到CupcakeAppBar
可组合函数中。
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
变量下方,使用名为currentScreen
的val
创建一个变量,并将其设为等于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 中处理了路线,并使用函数类型参数将导航逻辑与各个屏幕相隔离。您还学习了如何使用 intent 将数据发送到其他应用,以及如何自定义应用栏以响应导航。在接下来的几个单元中,您将学习开发其他复杂程度不断增加的多屏幕应用,并继续运用这些技能。