项目:Lunch Tray 应用

1. 准备工作

此 Codelab 将介绍如何自行构建一个名为 Lunch Tray 的新应用,并且会逐步引导您在 Android Studio 完成 Lunch Tray 应用项目,包括项目设置和测试。

此 Codelab 与本课程中的其他 Codelab 不同。与之前的 Codelab 不同,此 Codelab 的目的不是提供关于如何构建应用的分步教程。此 Codelab 旨在设置一个将由您独立完成的项目,它会为您提供有关如何完成应用及自行检查您的工作成果的说明。

我们在您要下载的应用中提供了测试套件,而不是解决方案代码。您将在 Android Studio 中运行这些测试(我们将在此 Codelab 稍后的部分介绍如何运行测试),并查看您的代码是否通过测试。您可能需要进行多次尝试,即使是专业开发者,也很少在第一次尝试时通过所有测试!只有在您的代码通过所有测试后,您才可以将此项目视为已完成。

我们理解,您可能只是想对照解决方案代码来进行检查。我们故意不提供解决方案代码,因为我们希望您像一名专业开发者一样进行练习。这可能需要您运用其他一些练习得并不多的技能,例如:

  • 使用 Google 搜索应用中出现的您不理解的术语、错误消息和代码段;
  • 测试代码,查看错误,然后更改代码并再次测试;
  • 复习前面学过的“Android 基础知识”相关内容;
  • 将您知道可以正常运行的代码(即项目中给出的代码,或您之前在第 3 单元中学过的其他应用的解决方案代码)与您编写的代码进行比较。

这最初可能会令人怯步,不过如果您能够完成第 3 单元,那么您已为完成此项目做好准备了,对此,我们有十足的信心。慢慢来,不要放弃。您可以完成此项目。

前提条件

  • 此项目的适用对象为已经完成《使用 Kotlin 进行 Android 开发的基础知识》课程第 3 单元的用户。

构建内容

  • 您将获取一款名为 Lunch Tray 的订餐应用,实现具有数据绑定的 ViewModel,并在 fragment 之间添加导航。

所需条件

  • 一台安装了 Android Studio 的计算机。

2. 已完成的应用概览

欢迎参与“Lunch Tray 项目!”

您可能也知道,导航是 Android 开发的基本组成部分。无论您是使用应用来浏览食谱,查找前往最喜爱餐馆的路线,或最重要的是订餐,您都可能需要在多个内容屏幕之间导航。在本项目中,您将利用第 3 单元学到的技能来构建一个名为 Lunch Tray 的午餐订购应用,实现视图模型、数据绑定以及屏幕之间的导航。

下面是最终的应用屏幕截图。用户首次启动 Lunch Tray 应用时,屏幕上会出现一个按钮,显示“Start Order”。

20fa769d4ba93ef3.png

点击 Start Order 后,用户便可从可用选项中选择一道主菜。用户可以更改所选主菜,此操作会更新底部显示的 Subtotal

438b61180d690b3a.png

在下一个屏幕上,用户可以添加配菜。

768352680759d3e2.png

然后,用户便可为订单选择佐餐食物。

8ee2bf41e9844614.png

最后,用户可以看到订单费用的汇总,细分为小计费用、销售税和总费用。还可以提交或取消订单。

61c883c34d94b7f7.png

这两个选项可将用户返回到第一个屏幕。如果用户提交了订单,屏幕底部应该会显示一个提示信息,告诉他们该订单已提交。

acb7d7a5d9843bac.png

3. 开始

下载项目代码

请注意,文件夹名称为 android-basics-kotlin-lunch-tray-app。在 Android Studio 中打开项目时,请选择此文件夹。

  1. 进入为此项目提供的 GitHub 代码库页面。
  2. 验证分支名称是否与此 Codelab 中指定的分支名称一致。例如,在以下屏幕截图中,分支名称为 main

1e4c0d2c081a8fd2.png

  1. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。

1debcf330fd04c7b.png

  1. 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。

8d1fda7396afe8e5.png

  1. 在文件浏览器中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行应用。请确保该应用按预期构建。

在开始实现 ViewModel 和导航之前,请花些时间确保项目构建成功,并熟悉该项目。首次运行该应用时,您会看到空白屏幕。MainActivity 不会呈现任何 fragment,因为您还没有设置导航图。

项目结构应该与您使用过的其他项目类似。为数据、模型和界面提供单独的软件包,并为资源提供单独的目录。

a19fd8a4bc92f2fc.png

用户可以订购的所有午餐选项(主菜、配菜和佐餐食物)均由 model 软件包中的 MenuItem 类表示。MenuItem 对象具有名称、说明、价格和类型。

data class MenuItem(
    val name: String,
    val description: String,
    val price: Double,
    val type: Int
) {
    fun getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(price)
}

该类型由来自 constant 软件包中 ItemType 对象的整数表示。

object ItemType {
    val ENTREE = 1
    val SIDE_DISH = 2
    val ACCOMPANIMENT = 3
}

您可以在 data 软件包中的 DataSource.kt 中找到单个 MenuItem 对象。

object DataSource {
    val menuItems = mapOf(
        "cauliflower" to
        MenuItem(
            name = "Cauliflower",
            description = "Whole cauliflower, brined, roasted, and deep fried",
            price = 7.00,
            type = ItemType.ENTREE
        ),
    ...
}

该对象只包含一个由键和对应的 MenuItem 组成的映射。您将从您首先实现的 ObjectViewModel 访问 DataSource

定义 ViewModel

如上一页面的屏幕截图所示,该应用要求用户提供三项内容:主菜、配菜和佐餐食物。然后,订单摘要屏幕会显示小计费用,并根据所选商品计算销售税,用于计算订单总费用。

model 软件包中,打开 OrderViewModel.kt,您会看到已经定义了几个变量。借助 menuItems 属性,您可以直接通过 ViewModel 访问 DataSource

val menuItems = DataSource.menuItems

首先,还有一些用于 previousEntreePricepreviousSidePricepreviousAccompanimentPrice 的变量。由于小计费用会在用户做出选择时(而不是在最终累加时)进行更新,因此,如果用户在移到下一个屏幕之前更改了选择,则这些变量将用于跟踪用户之前的选择。您将使用这些变量来确保小计费用已将之前和当前所选商品的价格差考虑在内。

private var previousEntreePrice = 0.0
private var previousSidePrice = 0.0
private var previousAccompanimentPrice = 0.0

此外,还有私有变量 _entree_side_accompaniment,用于存储当前选定的选项。类型为 MutableLiveData<MenuItem?>。每个私有变量都附带不可变类型 LiveData<MenuItem?> 的公开后备属性(entreesideaccompaniment)。这些可通过 fragment 的布局访问,以便在屏幕上显示所选的内容。LiveData 对象中包含的 MenuItem 也可为 null,因为用户无法选择主菜、配菜和/或佐餐食物。

// Entree for the order
private val _entree = MutableLiveData<MenuItem?>()
val entree: LiveData<MenuItem?> = _entree

// Side for the order
private val _side = MutableLiveData<MenuItem?>()
val side: LiveData<MenuItem?> = _side

// Accompaniment for the order.
private val _accompaniment = MutableLiveData<MenuItem?>()
val accompaniment: LiveData<MenuItem?> = _accompaniment

还有用于小计费用、总费用和税费的 LiveData 变量,它们使用数字格式,以便显示为货币。

// Subtotal for the order
private val _subtotal = MutableLiveData(0.0)
val subtotal: LiveData<String> = Transformations.map(_subtotal) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Total cost of the order
private val _total = MutableLiveData(0.0)
val total: LiveData<String> = Transformations.map(_total) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Tax for the order
private val _tax = MutableLiveData(0.0)
val tax: LiveData<String> = Transformations.map(_tax) {
    NumberFormat.getCurrencyInstance().format(it)
}

最后,税率是硬编码值 0.08 (8%)。

private val taxRate = 0.08

您需要实现 OrderViewModel 中的六个方法。

setEntree()、setSide() 和 setAccompaniment()

所有这些方法应分别以相同的方式适用于主菜、配菜和佐餐食物。例如,setEntree() 应执行以下操作:

  1. 如果 _entree 不是 null(即用户已选择主菜,但更改了自己的选择),请将 previousEntreePrice 设置为 current _entree 的价格。
  2. 如果 _subtotal 不是 null,请从小计费用中减去 previousEntreePrice
  3. _entree 的值更新为传递到函数的主菜(使用 menuItems 访问 MenuItem)。
  4. 调用 updateSubtotal(),传入新选择主菜的价格。

setSide()setAccompaniment() 的逻辑与 setEntree() 的实现相同。

updateSubtotal()

使用应添加到小计费用的新价格的参数调用 updateSubtotal()。此方法应执行以下三种操作:

  1. 如果 _subtotal 不是 null,请将 itemPrice 添加到 _subtotal
  2. 否则,如果 _subtotalnull,请将 _subtotal 设置为 itemPrice
  3. 设置(或更新)_subtotal 后,调用 calculateTaxAndTotal() 以更新这些值来反映新的小计费用。

calculateTaxAndTotal()

calculateTaxAndTotal() 应根据小计费用更新税费和总费用的变量。按如下方式实现该方法:

  1. _tax 设为税率乘以小计费用。
  2. _total 设置为小计费用加上税费。

resetOrder()

当用户提交或取消订单时,系统会调用 resetOrder()。您需要确保当用户开始新订单时,您的应用不会有任何剩余的数据。

通过将您在 OrderViewModel 中修改的所有变量设置回其原始值(0.0 或 null),实现 resetOrder()

创建数据绑定变量

在布局文件中实现数据绑定。打开布局文件,并添加 OrderViewModel 类型和/或相应的 fragment 类的数据绑定变量。

您需要实现所有 TODO 注释,才能在四个布局文件中设置文本并点击监听器:

  1. fragment_entree_menu.xml
  2. fragment_side_menu.xml
  3. fragment_accompaniment_menu.xml
  4. fragment_checkout.xml

在布局文件的 TODO 注释中列出了每个特定任务,但步骤总结如下。

  1. fragment_entree_menu.xml 中的 <data> 标记中,为 EntreeMenuFragment 添加绑定变量。对于每个单选按钮,您需要在 ViewModel 中选择按钮时设置主菜。小计费用文本视图的文本应进行相应的更新。此外,您还需要为 cancel_buttonnext_button 设置 onClick 属性,以分别取消订单或转到下一个屏幕。
  2. fragment_side_menu.xml 中执行相同的操作,为 SideMenuFragment 添加绑定变量,但在选中每个单选按钮时在视图模型中设置配菜。小计费用文本还需要进行更新,并且还需要为取消按钮和下一页按钮设置 onClick 属性。
  3. 再次执行相同的操作,但在 fragment_accompaniment_menu.xml 中,这次使用 AccompanimentMenuFragment 的绑定变量,在选中每个单选按钮时设置佐餐食物。同样,您还需要设置小计费用文本、取消按钮和下一页按钮的属性。
  4. fragment_checkout.xml 中,您需要添加 <data> 标记,以便定义绑定变量。在 <data> 标记中,添加两个绑定变量,一个用于 OrderViewModel,另一个用于 CheckoutFragment。在文本视图中,您需要设置从 OrderViewModel 中选择的主菜、配菜和佐餐食物的名称和价格。您还需要从 OrderViewModel 设置小计费用、税费和总费用。然后,使用 CheckoutFragment 中的相应函数,设置指定订单何时提交和何时取消的 onClickAttributes

.

初始化 fragment 中的数据绑定变量

初始化方法 onViewCreated() 中相应 fragment 文件中的数据绑定变量。

  1. EntreeMenuFragment
  2. SideMenuFragment
  3. AccompanimentMenuFragment
  4. CheckoutFragment

创建导航图

如您在第 3 单元中所学,导航图托管在相应 activity 所含的 FragmentContainerView 中。打开 activity_main.xml,然后将 TODO 替换为以下代码以声明 FragmentContainerView

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:name="androidx.navigation.fragment.NavHostFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:defaultNavHost="true"
   app:navGraph="@navigation/mobile_navigation"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintLeft_toLeftOf="parent"
   app:layout_constraintRight_toRightOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

导航图 mobile_navigation.xml 位于 res.navigation 软件包中。

e3381215c35c1726.png

这是应用的导航图。不过,该文件当前为空。您的任务是向导航图添加目的地,并对屏幕之间的以下导航进行建模。

  1. StartOrderFragment 转到 EntreeMenuFragment
  2. EntreeMenuFragment 转到 SideMenuFragment
  3. SideMenuFragment 转到 AccompanimentMenuFragment
  4. AccompanimentMenuFragment 转到 CheckoutFragment
  5. CheckoutFragment 转到 StartOrderFragment
  6. EntreeMenuFragment 转到 StartOrderFragment
  7. SideMenuFragment 转到 StartOrderFragment
  8. AccompanimentMenuFragment 转到 StartOrderFragment
  9. 起始目的地应为 StartOrderFragment

设置导航图后,您需要在 fragment 类中执行导航。在 fragment 中实现其余 TODO 注释,以及 MainActivity.kt

  1. 对于 EntreeMenuFragmentSideMenuFragmentAccompanimentMenuFragment 中的 goToNextScreen() 方法,转到应用中的下一个屏幕。
  2. 对于 EntreeMenuFragmentSideMenuFragmentAccompanimentMenuFragmentCheckoutFragment 中的 cancelOrder() 方法,首先在 sharedViewModel 中调用 resetOrder(),然后转到 StartOrderFragment
  3. StartOrderFragment 中,实现 setOnClickListener() 以转到 EntreeMenuFragment
  4. CheckoutFragment 中,实现 submitOrder() 方法。在 sharedViewModel 上调用 resetOrder(),然后转到 StartOrderFragment
  5. 最后,在 MainActivity.kt 中,将 navController 设置为 NavHostFragment 中的 navController

4. 测试应用

Lunch Tray 项目包含一个“androidTest”目标,其中包含多个测试用例:MenuContentTestsNavigationTestsOrderFunctionalityTests

运行测试

如需运行测试,您可以执行以下任一操作:

对于单个测试用例,打开测试用例类,并点击类声明左侧的绿色箭头。然后,从菜单中选择“Run”选项。这样会运行测试用例中的所有测试。

8ddcbafb8ec14f9b.png

经常会有您只想运行单个测试的情况,例如,有一个测试失败,而其他测试都通过。您可以像运行整个测试用例一样运行单个测试。点击绿色箭头并选择 Run 选项。

335664b7fc8b4fb5.png

如果您有多个测试用例,也可以运行整个测试套件。就像运行应用一样,您可以在 Run 菜单中找到此选项。

80312efedf6e4dd3.png

请注意,Android Studio 默认运行您运行的最后一个目标(应用目标、测试目标等),因此如果菜单仍然显示 Run > Run ‘app',您可以通过选择 Run > Run 运行测试目标。

95aacc8f749dee8e.png

然后从弹出式菜单中选择测试目标。

8b702efbd4d21d3d.png

5. 可选:向我们提供反馈!

我们非常期待收到您对此项目的反馈意见。请填写这份简短的调查问卷,向我们提供反馈 - 您的反馈将有助于指导我们将来为本课程创建项目。