使用 Compose 实现多屏幕导航

一、         实验名称

               使用 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,其中显示了不同的分享选项。

a32e016a6ccbf427.png

应用的当前状态存储在 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() {

}

        在枚举类中添加四种情况:StartFlavorPickup 和 Summary

enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

为应用添加 NavHost

NavHost 是一个可组合项,用于根据给定路线来显示其他可组合项目标页面。例如,如果路线为 FlavorNavHost 会显示用于选择纸杯蛋糕口味的屏幕。如果路线为 Summary,则应用会显示摘要屏幕。

NavHost 的语法与任何其他可组合项的语法一样。

fae7688d6dd53de9.png

有两个参数值得注意:

  • navControllerNavHostController 类的实例。您可以使用此对象在屏幕之间导航,例如,通过调用 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 接受函数类型作为其内容。

f67974b7fb3f0377.png

在 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 中的可组合项,接下来可以在不同屏幕之间导航了。NavHostControllerrememberNavController() 调用中的 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 类型。

8326701a77706258.png

        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() 方法即可。

fc8aae3911a6a25d.png

navigation 方法仅接受一个参数:与 NavHost 中定义的路线相对应的 String。如果路线与 NavHost 中的 composable() 任一调用匹配,应用便会转到该屏幕。

您将传入在用户按下 StartFlavor 和 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(),屏幕不仅会发生变化,而且会实际放置在返回堆栈之上。此外,当您点按系统返回按钮时,即可返回到上一个界面。

应用会将每个界面堆叠在上一个界面上,而返回按钮 (

bade5f3ecb71e4a2.png

) 可以移除这些界面。从底部 startDestination 到刚才显示的最顶部的屏幕的历史记录称为返回堆栈。

跳转至起始屏幕

与系统返回按钮不同,Cancel 按钮不会返回上一个屏幕。而是跳转移除返回堆栈中的所有屏幕,并返回起始屏幕。

您可以通过调用 popBackStack() 方法来实现此目的。

2f382e5eb319b4b8.png

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 的基本过程如下:

  1. 创建一个 intent 对象并指定 intent,例如 ACTION_SEND

  2. 指定随 intent 一同发送的其他数据类型。对于简单的一段文本,您可以使用 "text/plain",但也可以使用其他类型,例如 "image/*" 或 "video/*"

  3. 通过调用 putExtra() 方法,向 intent 传递任何其他数据,例如要分享的文本或图片。此 intent 将接受两个 extra:EXTRA_SUBJECT 和 EXTRA_TEXT

  4. 调用上下文的 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(),并传入 contextsubject 和 summary 作为参数。

onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}

        运行应用并在各个屏幕间导航。

点击 Send Order to Other App 时,您应当会看到底部动作条上的分享操作(例如 Messaging 和 Bluetooth)以及您以 extra 形式提供的主题和摘要。

a32e016a6ccbf427.png

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 以外的界面时,其中应当会显示向上按钮,点按该按钮可返回到上一个界面。

3fd023516061f522.gif

实验报告

一、程序代码

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结合使用,以便用户可以通过点击它来返回到前一个屏幕。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值