一、 实验名称
使用 Compose 实现多屏幕导航。
二、 参考资料
《Android开发者官方网站:Android 移动应用开发者工具 – Android 开发者 | Android Developers》、第6章课件。
三、 实验目的
本实验通过完成以下任务练习在Android应用中使用Compose实现多屏幕导航:
· 创建 NavHost 可组合项以定义应用中的路线和屏幕。
· 使用 NavHostController 在屏幕之间导航。
· 操控返回堆栈,以切换到之前的屏幕。
· 使用 intent 与其他应用共享数据。
· 自定义应用栏,包括标题和返回按钮。
四、 实验内容
1. 应用演示
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 按钮。
2. 定义路线并创建 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
参数。
注意:NavHostController 是 NavController 类的子类,可提供与 NavHost
可组合项搭配使用的额外功能。
您将向 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()
函数是 NavGraphBuilder 的扩展函数。
调用 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))
)
}
}
注意:quantityOptions
属性来自 DataSource.kt 中的 DataSource
单例对象。
在对 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
的全部内容。在下一部分中,您将让应用更改路线,并在用户点按每个按钮时在不同屏幕之间导航。
3. 在多个路线之间导航
现在,您已经定义了路线并将其映射到 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 按钮是否会返回第一个屏幕。
4. 导航到其他应用
到目前为止,您已经学习了如何导航到应用中的不同屏幕,以及如何返回主屏幕。在 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 形式提供的主题和摘要。
5. 让应用栏响应导航
尽管您的应用可以正常运行,并且可以在各个屏幕之间导航,但在此 Codelab 最开始的屏幕截图中仍缺少一些内容。应用栏无法自动响应导航。当应用导航至新路线时,不会更新标题,也不会适时在标题前显示向上按钮。
注意:系统返回按钮由 Android 操作系统提供,位于屏幕底部。
另一方面,向上按钮位于应用的 AppBar
中。
在您的应用上下文中,返回按钮和向上按钮的作用相同,都是返回上一个屏幕。
起始代码包含一个可组合项,用于管理名称为 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
以外的界面时,其中应当会显示向上按钮,点按该按钮可返回到上一个界面。
实验报告
一、程序代码
1. 在CupcakeScreen.kt代码中
package com.example.cupcake
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.example.cupcake.ui.OrderViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource
import androidx.compose.ui.platform.LocalContext
import com.example.cupcake.ui.SelectOptionScreen
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import com.example.cupcake.data.DataSource.flavors
import com.example.cupcake.ui.OrderSummaryScreen
import android.content.Context
import android.content.Intent
import androidx.annotation.StringRes
import androidx.navigation.compose.currentBackStackEntryAsState
/**
* Composable that displays the topBar and displays back button if back navigation is possible.
*/
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)
}
@Composable
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
) {
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)
)
}
}
}
)
}
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
navController: NavHostController = rememberNavController()
) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
Scaffold(
topBar = {
CupcakeAppBar(
currentScreen = currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = {navController.navigateUp() /* TODO: implement back navigation */ }
)
}
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
){
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) {
val context = LocalContext.current
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {cancelOrderAndNavigateToStart(viewModel, navController)},
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
},
modifier = Modifier.fillMaxHeight()
)
}
}
}
}
private fun shareOrder(context: Context, subject: String, summary: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
}
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
2. 在StartOrderScreen.kt代码中
package com.example.cupcake.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.cupcake.R
import com.example.cupcake.data.DataSource
import com.example.cupcake.ui.theme.CupcakeTheme
@Composable
fun StartOrderScreen(
quantityOptions: List<Pair<Int, Int>>,
onNextButtonClicked: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
) {
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_medium)))
Image(
painter = painterResource(R.drawable.cupcake),
contentDescription = null,
modifier = Modifier.width(300.dp)
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_medium)))
Text(
text = stringResource(R.string.order_cupcakes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_small)))
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
dimensionResource(id = R.dimen.padding_medium)
)
) {
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = {onNextButtonClicked(item.second)}
)
}
}
}
}
@Composable
fun SelectQuantityButton(
@StringRes labelResourceId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier.widthIn(min = 250.dp)
) {
Text(stringResource(labelResourceId))
}
}
@Preview
@Composable
fun StartOrderPreview() {
CupcakeTheme {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
3. 在SelectOptionScreen.kt代码中
package com.example.cupcake.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.example.cupcake.R
import com.example.cupcake.ui.components.FormattedPriceLabel
import com.example.cupcake.ui.theme.CupcakeTheme
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
) {
var selectedValue by rememberSaveable { mutableStateOf("") }
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))) {
options.forEach { item ->
Row(
modifier = Modifier.selectable(
selected = selectedValue == item,
onClick = {
selectedValue = item
onSelectionChanged(item)
}
),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedValue == item,
onClick = {
selectedValue = item
onSelectionChanged(item)
}
)
Text(item)
}
}
Divider(
thickness = dimensionResource(R.dimen.thickness_divider),
modifier = Modifier.padding(bottom = dimensionResource(R.dimen.padding_medium))
)
FormattedPriceLabel(
subtotal = subtotal,
modifier = Modifier
.align(Alignment.End)
.padding(
top = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.Bottom
) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
Button(
modifier = Modifier.weight(1f),
// the button is enabled when the user makes a selection
enabled = selectedValue.isNotEmpty(),
onClick = onNextButtonClicked
) {
Text(stringResource(R.string.next))
}
}
}
}
@Preview
@Composable
fun SelectOptionPreview() {
CupcakeTheme {
SelectOptionScreen(
subtotal = "299.99",
options = listOf("Option 1", "Option 2", "Option 3", "Option 4"),
modifier = Modifier.fillMaxHeight()
)
}
}
4. 在SummaryScreen.kt代码中
package com.example.cupcake.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import com.example.cupcake.R
import com.example.cupcake.data.OrderUiState
import com.example.cupcake.ui.components.FormattedPriceLabel
import com.example.cupcake.ui.theme.CupcakeTheme
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
) {
val resources = LocalContext.current.resources
val numberOfCupcakes = resources.getQuantityString(
R.plurals.cupcakes,
orderUiState.quantity,
orderUiState.quantity
)
//Load and format a string resource with the parameters.
val orderSummary = stringResource(
R.string.order_details,
numberOfCupcakes,
orderUiState.flavor,
orderUiState.date,
orderUiState.quantity
)
val newOrder = stringResource(R.string.new_cupcake_order)
//Create a list of order summary to display
val items = listOf(
// Summary line 1: display selected quantity
Pair(stringResource(R.string.quantity), numberOfCupcakes),
// Summary line 2: display selected flavor
Pair(stringResource(R.string.flavor), orderUiState.flavor),
// Summary line 3: display selected pickup date
Pair(stringResource(R.string.pickup_date), orderUiState.date)
)
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
) {
items.forEach { item ->
Text(item.first.uppercase())
Text(text = item.second, fontWeight = FontWeight.Bold)
Divider(thickness = dimensionResource(R.dimen.thickness_divider))
}
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_small)))
FormattedPriceLabel(
subtotal = orderUiState.price,
modifier = Modifier.align(Alignment.End)
)
}
Row(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Column(
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {onSendButtonClicked(newOrder, orderSummary)}
) {
Text(stringResource(R.string.send))
}
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
}
}
}
}
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
二、 实验结果(含程序运行截图)
1. 运行应用并在各个屏幕间导航。
点击 Send Order to Other App 时,您应当会看到底部动作条上的分享操作(例如 Messaging 和 Bluetooth)以及您以 extra 形式提供的主题和摘要。
尽管您的应用可以正常运行,并且可以在各个屏幕之间导航,但在此 Codelab 最开始的屏幕截图中仍缺少一些内容。应用栏无法自动响应导航。当应用导航至新路线时,不会更新标题,也不会适时在标题前显示向上按钮。
2. 让应用栏响应导航
运行应用。AppBar 标题现已更新以反映当前屏幕。当您导航到 StartOrderScreen 以外的界面时,其中应当会显示向上按钮,点按该按钮可返回到上一个界面。
三、 出现问题及解决方法
1. 导航目的地未找到
错误描述:当尝试导航到一个不存在的目的地时,应用可能会崩溃或显示一个错误消息。
分析:确保你在NavHost中定义了所有导航目的地,并且它们的路由名称与你在navigate方法中使用的名称完全匹配。同时,检查你的navigate调用是否在正确的上下文中,比如在一个可点击的组件的事件处理器中。
2. 参数传递错误
错误描述:在传递参数给导航目的地时,可能会出现参数类型不匹配、参数未找到或参数格式错误等问题。
分析:确保你在导航时传递的参数与接收屏幕期望的参数类型和格式相匹配。如果接收屏幕使用了arguments来获取参数,请确保传递的参数名称与接收屏幕中定义的参数名称一致。
3. 导航控制器未正确初始化
错误描述:在尝试使用NavHostController进行导航时,可能会出现空指针异常或其他初始化错误。
分析:意味着你尝试在NavHost的作用域之外访问或使用了NavHostController,或者在初始化NavHostController时发生了错误。确保NavHostController是在NavHost的作用域内初始化的;NavHostController的正确引用。
四、 实验心得
在Android应用中使用Compose实现多屏幕导航:
1. 首先,你需要在项目根目录 build.gradle 文件中添加 Compose 和 Navigation 的依赖项。
2. 设置和配置你的应用以实现导航功能
(1)创建NavHost
使用NavHost组件作为导航宿主。NavHost是导航图的根,会根据当前的导航状态显示相应的屏幕。可以使用NavHost的startDestination属性来指定应用启动时显示的默认屏幕。
(2)定义导航图
创建多个NavGraph对象来定义应用导航结构。这些对象描述了不同屏幕之间的关系以及如何从一个屏幕导航到另一个屏幕,在NavGraph中,可以使用composable函数来指定每个屏幕对应的Compose函数。
(3)添加导航动作
使用navController对象来触发导航动作。如调用如navigate、popBackStack等方法。
3. 完善导航逻辑(操控返回堆栈,以切换到之前的屏幕)
(1)处理导航事件
确保按钮、列表项或其他可交互组件正确地触发了导航动作。如设置onClick或其他事件监听器,并在其中调用navController的navigate方法来执行导航。
(2)传递参数
如果在导航到新屏幕时需要传递参数(数据或配置选项),可以在navigate方法中指定这些参数。接收屏幕可以通过arguments参数访问这些传递的参数。
(3)处理返回和向上导航
确保导航图支持向上导航,以便用户可以返回到前一个屏幕。
(4)当前的导航状态更新UI以反映导航状态:
4. 使用 intent 与其他应用共享数据。
5. 自定义应用栏,包括标题和返回按钮。
使用TopAppBar来创建自定义的应用栏。返回按钮通常与NavHostController结合使用,以便用户可以通过点击它来返回到前一个屏幕。