Fragment 之间的共享 ViewModel

1. 准备工作

您已经学习了如何使用 activity、fragment、intent、数据绑定和导航组件,并学习了架构组件的基础知识。在此 Codelab 中,您要将学到的东西整合到一起,创建一个高级示例,即一个纸杯蛋糕订购应用。

您将学习如何使用共享 ViewModel 在同一 activity 的 fragment 之间共享数据,还会学习 LiveData 转换等新概念。

前提条件

  • 可以轻松自如地阅读和理解 XML 中的 Android 布局
  • 熟悉 Jetpack Navigation 组件的基础知识
  • 能够在应用中创建带有 fragment 目的地的导航图
  • 在 activity 中使用过 fragment
  • 能够创建 ViewModel 来存储应用数据
  • 能够将数据绑定与 LiveData 结合使用,实现界面与 ViewModel 中的应用数据保持同步

学习内容

  • 如何在更高级的用例中采取建议的应用架构做法
  • 如何在同一 activity 的多个 fragment 之间使用共享 ViewModel
  • 如何应用 LiveData 转换

构建内容

  • 一个 Cupcake 应用,它显示纸杯蛋糕的订购流程,可让用户选择纸杯蛋糕口味、数量和取货日期。

所需条件

  • 一台安装了 Android Studio 的计算机。
  • Cupcake 应用的起始代码

2. 起始应用概览

Cupcake 应用概览

Cupcake 应用演示了如何设计和实现在线订购应用。此在线课程结束时,您将完成包含以下屏幕的 Cupcake 应用。用户可以在纸杯蛋糕订单中选择纸杯蛋糕的数量、口味及其他选项。

732881cfc463695d.png

下载此 Codelab 的起始代码

此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码将包含您在之前的 Codelab 中已熟悉的代码。

如果您从 GitHub 下载起始代码,那么请注意,项目的文件夹名称为 android-basics-kotlin-cupcake-app-starter。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。

5b0a76c50478a73f.png

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

在 Android Studio 中打开项目

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

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

21f3eec988dcfbe9.png

  1. Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用按预期构建。
  5. Project 工具窗口中浏览项目文件,了解应用的设置方式。

起始代码演示

  1. 在 Android Studio 中打开下载的项目。该项目的文件夹名称为 android-basics-kotlin-cupcake-app-starter。然后,运行应用。
  2. 浏览文件以了解起始代码。对于布局文件,您可以使用右上角的 Split 选项同时查看布局和 XML 的预览。
  3. 编译和运行应用时,您会注意到应用不完整。按钮没有太大的用处(除了用于显示 Toast 消息),您无法导航到其他 fragment。

下面是项目中重要文件的演示。

MainActivity:

MainActivity 的代码与默认生成的代码类似,它会将 activity 的内容视图设为 activity_main.xml。此代码使用参数化构造函数 AppCompatActivity(@LayoutRes int contentLayoutId),它接受一个布局,该布局将作为 super.onCreate(savedInstanceState) 的一部分膨胀。

MainActivity 类中的代码

class MainActivity : AppCompatActivity(R.layout.activity_main)

与使用默认 AppCompatActivity 构造函数的以下代码相同:

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

布局(res/layout 文件夹):

layout 资源文件夹包含 activity 和 fragment 布局文件。这些是简单的布局文件,您在之前的 Codelab 中已熟悉 XML。

  • fragment_start.xml 是应用中显示的第一个屏幕。它包含一张纸杯蛋糕图片和三个按钮,这三个按钮用于选择要订购的纸杯蛋糕数量:1 个纸杯蛋糕、6 个纸杯蛋糕和 12 个纸杯蛋糕。
  • fragment_flavor.xml 将纸杯蛋糕口味的列表显示为单选按钮选项,还显示了一个 Next 按钮。
  • fragment_pickup.xml 提供了一个用于选择取货日期的选项,还有一个用于转到摘要屏幕的 Next 按钮。
  • fragment_summary.xml 显示了订单详情的摘要(如数量和口味),还有一个用于将订单发送到另一个应用的按钮。

Fragment 类:

  • StartFragment.kt 是应用中显示的第一个屏幕。这个类包含视图绑定代码和三个按钮的点击处理程序。
  • FlavorFragment.ktPickupFragment.ktSummaryFragment.kt 类主要包含样板代码以及 NextSend Order to Another App 按钮的点击处理程序,点击这些按钮会显示消息框消息。

资源(res 文件夹):

  • drawable 文件夹包含第一个屏幕的纸杯蛋糕资源,以及启动器图标文件。
  • navigation/nav_graph.xml 包含四个 fragment 目的地(startFragmentflavorFragmentpickupFragmentsummaryFragment),但不包含操作,您稍后将在此 Codelab 中定义操作。
  • values 文件夹包含用于自定义应用主题的颜色、尺寸、字符串、样式和主题。您应该在之前的 Codelab 中已熟悉这些资源类型。

3. 完成导航图

在此任务中,您会将 Cupcake 应用的屏幕连接在一起,并在应用中实现适当的导航。

您还记得我们使用 Navigation 组件需要满足的条件吗?按照此指南中的说明操作,复习一下如何设置项目和应用,以便:

  • 添加 Jetpack Navigation 库
  • 向 activity 添加 NavHost
  • 创建导航图
  • 向导航图添加 fragment 目的地

连接导航图中的目的地

  1. 在 Android Studio 的 Project 窗口中,依次打开 res > navigation > nav_graph.xml 文件。切换到 Design 标签页(如果尚未选择该标签页)。

28c2c94eb97e2f0.png

  1. 此时将打开 Navigation Editor,以直观呈现应用中的导航图。您应该会看到应用中已存在的四个 fragment。

fdce89b318218ea6.png

  1. 连接导航图中的 fragment 目的地。创建从 startFragmentflavorFragment 的操作,从 flavorFragmentpickupFragment 的连接,以及从 pickupFragmentsummaryFragment 的连接。如果您需要更详细的说明,请按照接下来的几个步骤操作。
  2. 将光标悬停在 startFragment 上,直到您看到该 fragment 周围的灰色边框,以及出现在该 fragment 右侧边缘中心的灰色圆圈。点击该圆圈并将其拖动到 flavorFragment,然后松开鼠标。

d014c1b710c1088d.png

  1. 这两个 fragment 之间的箭头指示连接成功,这表示您将能够从 startFragment 导航到 flavorFragment。这称为导航操作,您在之前的 Codelab 中已经学过。

65c7d993b98c9dea.png

  1. 同样,添加从 flavorFragmentpickupFragment 以及从 pickupFragmentsummaryFragment 的导航操作。当您创建完导航操作后,完成的导航图应如下所示。

724eb8992a1a9381.png

  1. 您创建的三项新操作也应该反映在 Component Tree 窗格中。

e4ee54469f5ff1a4.png

  1. 定义导航图时,您还需要指定起始目的地。目前,您可以看到 startFragment 旁边有一个小房子图标。

739d4ddac561c478.png

这指示 startFragment 将是要在 NavHost 中显示的第一个 fragment。保留此设置作为应用的预期行为。为了方便您日后参考,您可以随时更改起始目的地,方法是右键点击一个 fragment,然后选择菜单选项 Set as Start Destination

bf3cfa7841476892.png

接下来,您将添加相应的代码,以通过点按第一个 fragment 中的按钮从 startFragment 导航到 flavorFragment,而不是显示 Toast 消息。下面是起始 fragment 布局的参考。您将在后续任务中将纸杯蛋糕的数量传递给口味 fragment。

867d8e4c72078f76.png

  1. Project 窗口中,依次打开 app > java > com.example.cupcake > StartFragment Kotlin 文件。
  2. onViewCreated() 方法中,请注意,在三个按钮上设置了点击监听器。点按每个按钮时,系统会调用 orderCupcake() 方法,并将纸杯蛋糕的数量(1 个、6 个或 12 个纸杯蛋糕)作为其形参。

参考代码

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. orderCupcake() 方法中,将用于显示消息框消息的代码替换为用于导航到口味 fragment 的代码。使用 findNavController() 方法获取 NavController 并对其调用 navigate(),且传入操作 ID R.id.action_startFragment_to_flavorFragment。确保此操作 ID 与 nav_graph.xml. 中声明的操作匹配。

将以下代码

fun orderCupcake(quantity: Int) {
    Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}

替换为下面的代码:

fun orderCupcake(quantity: Int) {
   findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. 添加导入代码 import androidx.navigation.fragment.findNavController,或者您也可以从 Android Studio 提供的选项中进行选择。

2a087f53a77765a6.png

向口味 fragment 和取货 fragment 添加导航

与前面的任务类似,在此任务中,您将向其他 fragment(即口味 fragment 和取货 fragment)添加导航。

3b351067bf4926b7.png

  1. 依次打开 app > java > com.example.cupcake > FlavorFragment.kt。请注意,在 Next 按钮点击监听器中调用的方法是 goToNextScreen() 方法。
  2. FlavorFragment.kt 中的 goToNextScreen() 方法内,替换用于显示消息框的代码以导航到取货 fragment。使用操作 ID R.id.action_flavorFragment_to_pickupFragment,并确保此 ID 与 nav_graph.xml. 中声明的操作匹配。
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

记得添加 import androidx.navigation.fragment.findNavController 代码以导入相应的类。

  1. 同样,在 PickupFragment.kt 中的 goToNextScreen() 方法内,替换现有代码以导航到摘要 fragment。
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

导入 androidx.navigation.fragment.findNavController

  1. 运行应用。确保使用按钮能够在屏幕之间导航。每个 fragment 中显示的信息可能不完整,但不必担心,您将在接下来的步骤中以正确的数据填充这些 fragment。

96b33bf7a5bd8050.png

更新应用栏中的标题

当您在应用中导航时,请注意应用栏中的标题。它始终显示为 Cupcake

如果能够根据当前 fragment 的功能来提供更相关的标题,那么就会带来更好的用户体验。

使用 NavController 为每个 fragment 更改应用栏(又称为操作栏)中的标题,并显示 Up (←) 按钮。

b7657cdc50cfeab0.png

  1. MainActivity.kt 中,替换 onCreate() 方法以设置导航控制器。从 NavHostFragment 获取 NavController 的实例。
  2. setupActionBarWithNavController(navController) 进行调用,并传入 NavController 的实例。这会有以下作用:根据目的地的标签,在应用栏中显示标题;只要您不在顶级目的地,就会显示 Up 按钮。
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. 当 Android Studio 提示时,添加必要的导入代码。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. 为每个 fragment 设置应用栏标题。打开 navigation/nav_graph.xml 并切换到 Code 标签页。
  2. nav_graph.xml 中,修改每个 fragment 目的地的 android:label 属性。使用起始应用中已声明的以下字符串资源。

对于起始 fragment,使用 @string/app_name,值为 Cupcake

对于口味 fragment,使用 @string/choose_flavor,值为 Choose Flavor

对于取货 fragment,使用 @string/choose_pickup_date,值为 Choose Pickup Date

对于摘要 fragment,使用 @string/order_summary,值为 Order Summary

<navigation ...>
    <fragment
        android:id="@+id/startFragment"
        ...
        android:label="@string/app_name" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        ...
        android:label="@string/choose_flavor" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment"
        ...
        android:label="@string/choose_pickup_date" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment"
        ...
        android:label="@string/order_summary" ... />
</navigation>
  1. 运行应用。请注意,当您导航到每个 fragment 目的地时,应用栏中的标题会发生变化。另请注意,应用栏中现在显示了 Up 按钮(箭头 ←)。如果您点按该按钮,它不起任何作用。您将在下一个 Codelab 中实现 Up 按钮行为。

89e0ea37d4146271.png

4. 创建共享 ViewModel

让我们继续在每个 fragment 中填充正确的数据。您将使用共享 ViewModel 将应用的数据保存在单个 ViewModel 中。应用中的多个 fragment 将根据其 activity 作用域访问共享 ViewModel

在大多数正式版应用中,在 fragment 之间共享数据是常见的用例。例如,在此 Codelab 中的最终版 Cupcake 应用中(请注意下面的屏幕截图),用户在第一个屏幕中选择纸杯蛋糕的数量,在第二个屏幕中,系统根据纸杯蛋糕的数量计算并显示价格。同样,口味和取货日期等其他应用数据也用在摘要屏幕中。

3b6a68cab0b9ee2.png

通过查看应用功能,您可以推断,将此订单信息存储在单个 ViewModel 中会很有用,该视图模型可以在此 activity 的 fragment 之间共享。回想一下,ViewModelAndroid 架构组件的一部分。保存在 ViewModel 中的应用数据在配置更改期间会保留。如需将 ViewModel 添加到应用,您可以创建一个从 ViewModel 类扩展的新类。

创建 OrderViewModel

在此任务中,您将为 Cupcake 应用创建一个名为 OrderViewModel 的共享 ViewModel。您还会将应用数据添加为 ViewModel 内的属性,并添加用于更新和修改数据的方法。下面是该类的属性:

  • 订购数量 (Integer)
  • 纸杯蛋糕口味 (String)
  • 取货日期 (String)
  • 价格 (Double)

遵循 ViewModel 最佳实践

ViewModel 中,建议的做法是不将视图模型数据公开为 public 变量。否则,应用数据可能会被外部类以意想不到的方式修改,并造成应用没有预料到要处理的极端情况。应将这些可变属性设为 private,实现后备属性,并在需要时公开每个属性的 public 不可变版本。惯例是在 private 可变属性的名称前面加上下划线 (_) 作为前缀。

下面是用于更新上述属性的方法,具体取决于用户的选择:

  • setQuantity(numberCupcakes: Int)
  • setFlavor(desiredFlavor: String)
  • setDate(pickupDate: String)

您不需要使用 setter 方法来设置价格,因为您将使用其他属性在 OrderViewModel 中计算价格。下面的步骤为您演示了如何实现共享 ViewModel

您将在项目中创建一个名为 model 的新软件包,并添加 OrderViewModel 类。这样会将视图模型代码与界面代码的其余部分(fragment 和 activity)分离开来。根据功能将代码分离到软件包中是一种编码最佳实践。

  1. 在 Android Studio 的 Project 窗口中,右键点击 com.example.cupcake > New > Package
  2. 此时将打开 New Package 对话框,请将软件包命名为 com.example.cupcake.model

d958ee5f3d2aef5a.png

  1. model 软件包下创建 OrderViewModel Kotlin 类。在 Project 窗口中,右键点击 model 软件包,然后依次选择 New > Kotlin File/Class。在新对话框中,提供文件名 OrderViewModel

fc68c1d3861f1cca.png

  1. OrderViewModel.kt 中,更改类签名以从 ViewModel 扩展。
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. OrderViewModel 类内,将上述属性添加为 private val
  2. 将属性类型更改为 LiveData 并向属性添加后备字段,这样这些属性就可观察,当视图模型中的源数据发生变化时,界面就会更新。
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price

您需要导入以下类:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
  1. OrderViewModel 类中,添加上述方法。在方法内,分配传入可变属性的参数。
  2. 由于这些 setter 方法需要从视图模型外部进行调用,因此请将其保留为 public 方法(这意味着,在 fun 关键字之前不需要 private 或其他可见性修饰符)。Kotlin 中的默认可见性修饰符为 public
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
}

fun setFlavor(desiredFlavor: String) {
    _flavor.value = desiredFlavor
}

fun setDate(pickupDate: String) {
    _date.value = pickupDate
}
  1. 构建并运行您的应用,以确保没有编译错误。界面中应该还没有明显的变化。

非常棒!现在,您已经有了视图模型的起始代码。随着您在应用中构建出更多功能并意识到类中需要更多属性和方法,您会逐渐向此类添加更多代码。

如果您看到类名称、属性名称或方法名称在 Android Studio 中显示为灰色字体,这并不奇怪。这意味着,类、属性或方法目前没有被使用,但它们将会被使用!这是接下来要讲的内容。

5. 使用 ViewModel 更新界面

在此任务中,您将使用创建的共享视图模型更新应用的界面。实现共享视图模型的主要区别在于我们从界面控制器访问它的方式。您将使用 activity 实例而不是 fragment 实例,您将在接下来几部分中看到如何执行此操作。

这意味着,视图模型可以在 fragment 之间共享。每个 fragment 都可以访问视图模型,以检查订单的某些详情或更新视图模型中的某些数据。

更新 StartFragment 以使用视图模型

为了在 StartFragment 中使用共享视图模型,您将使用 activityViewModels() 而不是 viewModels() 委托类来初始化 OrderViewModel

  • viewModels() 可为您提供作用域限定为当前 fragment 的 ViewModel 实例。这会因 fragment 不同而异。
  • activityViewModels() 可为您提供作用域限定为当前 activity 的 ViewModel 实例。因此,实例将在同一 activity 的多个 fragment 之间保持不变。

使用 Kotlin 属性委托

在 Kotlin 中,每个可变 (var) 属性都具有自动为其生成的默认 getter 和 setter 函数。当您为该属性赋值或读取其值时,系统会调用 setter 和 getter 函数。(对于只读属性 [val],默认情况下仅为其生成 getter 函数。当您读取只读属性的值时,系统会调用此 getter 函数。)

Kotlin 中的属性委托可以帮助您将 getter-setter 的责任移交给另一个类。

此类(称为“委托类”)提供属性的 getter 和 setter 函数并处理其变更。

委托属性使用 by 子句和委托类实例进行定义:

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
  1. StartFragment 类中,以类变量的形式获取对共享视图模型的引用。使用 fragment-ktx 库中的 by activityViewModels() Kotlin 属性委托。
private val sharedViewModel: OrderViewModel by activityViewModels()

您可能需要导入下面这些新类:

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. 针对 FlavorFragmentPickupFragmentSummaryFragment 类重复执行上面的步骤,您将在此 Codelab 的后面几部分中使用此 sharedViewModel 实例。
  2. 返回 StartFragment 类,您现在可以使用视图模型了。在 orderCupcake() 方法的开头,调用共享视图模型中的 setQuantity() 方法以更新数量,然后再导航到口味 fragment。
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. OrderViewModel 类中,添加以下方法来检查是否已设置订购的纸杯蛋糕口味。在后面的步骤中,您将在 StartFragment 类中使用此方法。
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. StartFragment 类中的 orderCupcake() 方法内,设置数量后,将默认口味设置为“Vanilla”(如果未设置口味),然后再导航到口味 fragment。完整的方法将如下所示:
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    if (sharedViewModel.hasNoFlavorSet()) {
        sharedViewModel.setFlavor(getString(R.string.vanilla))
    }
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. 构建应用以确保没有编译错误。不过,界面中应该没有明显的变化。

6. 将 ViewModel 与数据绑定搭配使用

接下来,您将使用数据绑定将视图模型数据绑定到界面。您还将根据用户在界面中做出的选择来更新共享视图模型。

回顾数据绑定

回想一下,数据绑定库Android Jetpack 的一部分。数据绑定使用声明性格式将布局中的界面组件绑定到应用中的数据源。简而言之,数据绑定就是将数据(从代码)绑定到视图 + 视图绑定(将视图绑定到代码)。通过设置这些绑定并让更新自动执行,可帮助您降低在忘记从代码中手动更新界面时出现错误的几率。

根据用户选择更新口味

  1. layout/fragment_flavor.xml 中,在根 <layout> 标记内添加一个 <data> 标记。添加一个名为 viewModel 且类型为 com.example.cupcake.model.OrderViewModel 的布局变量。请确保类型属性中的软件包名称与应用中的共享视图模型类 OrderViewModel 的软件包名称相符。
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. 同样,针对 fragment_pickup.xmlfragment_summary.xml 重复执行上面的步骤以添加 viewModel 布局变量。您将在后面几部分中使用此变量。您不需要在 fragment_start.xml 中添加此代码,因为此布局不使用共享视图模型。
  2. FlavorFragment 类中的 onViewCreated() 内,将视图模型实例与布局中的共享视图模型实例绑定。在 binding?.apply 代码块内添加以下代码。
binding?.apply {
    viewModel = sharedViewModel
    ...
}

apply 作用域函数

这可能是您首次看到 Kotlin 中的 apply 函数。apply 是 Kotlin 标准库中的作用域函数它在对象的上下文中执行代码块。它会形成一个临时作用域,在该作用域中,您可以访问对象而不需要其名称。apply 的常见用例是配置对象。此类调用可以解读为“对对象应用以下赋值”。

示例

clark.apply {
    firstName = "Clark"
    lastName = "James"
    age = 18
}

// The equivalent code without apply scope function would look like the following.

clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
  1. 针对 PickupFragmentSummaryFragment 类中的 onViewCreated() 方法重复执行相同的步骤。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. fragment_flavor.xml 中,根据视图模型中的 flavor 值,使用新布局变量 viewModel 来设置单选按钮的 checked 属性。如果由单选按钮表示的口味与视图模型中保存的口味相同,则将单选按钮显示为选中状态 (checked = true)。Vanilla RadioButton 的选中状态的绑定表达式将如下所示:

@{viewModel.flavor.equals(@string/vanilla)}

从本质上讲,您是使用 equals 函数将 viewModel.flavor 属性与相应的字符串资源进行比较,以确定选中状态应该是 true 还是 false。

<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:checked="@{viewModel.flavor.equals(@string/coffee)}"
       .../>
</RadioGroup>

监听器绑定

监听器绑定是在事件(如 onClick 事件)发生时运行的 lambda 表达式。它们类似于方法引用(如 textview.setOnClickListener(clickListener)),但监听器绑定可让您运行任意数据绑定表达式。

  1. fragment_flavor.xml 中,使用监听器绑定向单选按钮添加事件监听器。使用不带形参的 lambda 表达式,并通过传入相应的口味字符串资源对 viewModel.setFlavor() 方法进行调用。
<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
       .../>
</RadioGroup>
  1. 运行应用,请注意 Vanilla 选项在口味 fragment 中默认处于选中状态。

3095e824b4817b98.png

太好了!现在,您可以继续构建后面的 fragment 了。

7. 更新取货 fragment 和汇总 fragment,以使用视图模型

在应用中导航,请注意,在取货 fragment 中,单选按钮选项标签是空白的。在此任务中,您将计算 4 个可用的取货日期,并将其显示在取货 fragment 中。您可以采用不同的方式来显示设置了格式的日期,下面是 Android 为此提供的一些有用的实用程序。

创建取货选项列表

日期格式设置工具

Android 框架提供了一个名为 SimpleDateFormat 的类,这个类用于以语言区域敏感的方式对日期进行格式设置和解析。使用该类可以对日期进行格式设置(日期 → 文本)和解析(文本 → 日期)。

您可以通过传入模式字符串和语言区域来创建 SimpleDateFormat 的实例:

SimpleDateFormat("E MMM d", Locale.getDefault())

"E MMM d" 这样的模式字符串表示日期和时间格式。字母 'A''Z' 以及 'a''z' 解释为模式字母,它们表示日期或时间字符串的组成部分。例如,d 表示几号,y 表示年份,M 表示月份。如果日期是 2018 年 1 月 4 日,模式字符串 "EEE, MMM d" 将解析为 "Wed, Jul 4"。如需查看模式字母的完整列表,请参阅相应的文档

Locale 对象表示特定的地理、政治或文化区域。它表示语言/国家/地区/变体的组合。语言区域用于改变数字或日期等信息的表示方式,使其符合相应区域的惯例。日期和时间对语言区域敏感,因为它们在世界上不同地方的书写方式不同。您将使用 Locale.getDefault() 方法检索在用户设备上设置的语言区域信息,并将其传入 SimpleDateFormat 构造函数。

Android 中的语言区域是语言和国家/地区代码的组合。语言代码是两个字母的小写 ISO 语言代码,如“en”表示英语。国家/地区代码是两个字母的大写 ISO 国家/地区代码,如“US”表示美国。

现在,使用 SimpleDateFormatLocale 确定 Cupcake 应用的可用取货日期。

  1. OrderViewModel 类中,添加以下名为 getPickupOptions() 的函数,以创建并返回取货日期列表。在该方法中,创建一个名为 optionsval 变量,并将其初始化为 mutableListOf<String>()
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. 使用 SimpleDateFormat 创建格式设置工具字符串,并传递模式字符串 "E MMM d" 和语言区域。在模式字符串中,E 代表星期几,它解析为“Tue Dec 10”。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

当 Android Studio 提示时,导入 java.text.SimpleDateFormatjava.util.Locale

  1. 获取 Calendar 实例并将其分配给一个新变量。将此变量设为 val。此变量将包含当前日期和时间。此外,还应导入 java.util.Calendar
val calendar = Calendar.getInstance()
  1. 构建一个日期列表,从当前日期开始,还有接下来的三个日期。由于您需要 4 个日期选项,因此请重复此代码块 4 次。此 repeat 代码块会设置日期的格式,将其添加到日期选项列表,然后让日历按 1 天递增。
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  1. 在方法的末尾返回更新后的 options。下面是您完成的方法:
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
   val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
   val calendar = Calendar.getInstance()
   // Create a list of dates starting with the current date and the following 3 dates
   repeat(4) {
       options.add(formatter.format(calendar.time))
       calendar.add(Calendar.DATE, 1)
   }
   return options
}
  1. OrderViewModel 类中,添加一个名为 dateOptions 的类属性,它是一个 val。使用您刚刚创建的 getPickupOptions() 方法对其进行初始化。
val dateOptions = getPickupOptions()

更新布局以显示取货选项

现在,视图模型中已经有了四个可用的取货日期,下面更新 fragment_pickup.xml 布局以显示这些日期。您还将使用数据绑定显示每个单选按钮的选中状态,并在选中了其他单选按钮时更新视图模型中的日期。此实现类似于口味 fragment 中的数据绑定。

fragment_pickup.xml 中:

单选按钮 option0 表示 viewModel 中的 dateOptions[0](今天)

单选按钮 option1 表示 viewModel 中的 dateOptions[1](明天)

单选按钮 option2 表示 viewModel 中的 dateOptions[2](后天)

单选按钮 option3 表示 viewModel 中的 dateOptions[3](大后天)

  1. fragment_pickup.xml 中,对于 option0 单选按钮,根据视图模型中的 date 值,使用新布局变量 viewModel 来设置 checked 属性。将 viewModel.date 属性与 dateOptions 列表中的第一个字符串(即当前日期)进行比较。使用 equals 函数进行比较,最终的绑定表达式如下所示:

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. 对于同一单选按钮,使用监听器绑定向 onClick 属性添加事件监听器。点击此单选按钮选项时,在 viewModel 上对 setDate() 进行调用,并传入 dateOptions[0]
  2. 对于同一单选按钮,将 text 属性值设置为 dateOptions 列表中的第一个字符串。
<RadioButton
   android:id="@+id/option0"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
   android:text="@{viewModel.dateOptions[0]}"
   ...
   />
  1. 针对其他单选按钮重复执行上面的步骤,相应地更改 dateOptions 的索引。
<RadioButton
   android:id="@+id/option1"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
   android:text="@{viewModel.dateOptions[1]}"
   ... />

<RadioButton
   android:id="@+id/option2"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
   android:text="@{viewModel.dateOptions[2]}"
   ... />

<RadioButton
   android:id="@+id/option3"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
   android:text="@{viewModel.dateOptions[3]}"
   ... />
  1. 运行应用,您应该会看到接下来几天作为可用的取货选项。您的屏幕截图会因您的当前日期而异。请注意,默认情况下未选中任何选项。您将在下一步中实现此行为。

b55b3a36e2aa7be6.png

  1. OrderViewModel 类中,创建一个名为 resetOrder() 的函数,以重置视图模型中的 MutableLiveData 属性。将 dateOptions 列表中的当前日期值赋给 _date.value.
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. 向类中添加一个 init 代码块,并从其调用新方法 resetOrder()
init {
   resetOrder()
}
  1. 从类的属性声明中移除初始值。现在,创建 OrderViewModel 的实例时,您使用 init 代码块来初始化属性。
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
  1. 再次运行应用,请注意,默认情况下选中了今天的日期。

bfe4f1b82977b4bc.png

更新汇总 fragment 以使用视图模型

现在,让我们继续构建最后一个 fragment。订单摘要 fragment 旨在显示订单详情的摘要。在此任务中,您将利用共享视图模型中的所有订单信息,并使用数据绑定更新屏幕上的订单详情。

78f510e10d848dd2.png

  1. fragment_summary.xml 中,确保您已声明视图模型数据变量 viewModel
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. SummaryFragmentonViewCreated() 中,确保已初始化 binding.viewModel
  2. fragment_summary.xml 中,读取视图模型中的数据,以使用订单摘要详情更新屏幕。通过添加以下文本属性,更新数量、口味和日期 TextViews。数量的类型为 Int,因此您需要将其转换为字符串。
<TextView
   android:id="@+id/quantity"
   ...
   android:text="@{viewModel.quantity.toString()}"
   ... />
<TextView
   android:id="@+id/flavor"
   ...
   android:text="@{viewModel.flavor}"
   ... />
<TextView
   android:id="@+id/date"
   ...
   android:text="@{viewModel.date}"
   ... />
  1. 运行并测试应用以验证您选择的订单选项是否显示在订单摘要中。

7091453fa817b55.png

8. 根据订单详情计算价格

看一下此 Codelab 的最终应用屏幕截图,您会注意到,价格确实显示在每个 fragment 中(StartFragment 除外),这样用户在创建订单时就会知道价格。

3b6a68cab0b9ee2.png

下面是纸杯蛋糕店关于如何计算价格的规则。

  • 每个纸杯蛋糕 $2.00
  • 如果当天取货,订单价格另加 $3.00

因此,如果订购了 6 个纸杯蛋糕,那么价格将为 6 个纸杯蛋糕 x $2/个 = $12。如果用户想要当天取货,那么需要额外支付 $3,这样一来,订单总价就为 $15。

更新视图模型中的价格

如需在应用中添加对此功能的支持,请先处理每个纸杯蛋糕的价格,暂时忽略当天取货费用。

  1. 打开 OrderViewModel.kt,将每个纸杯蛋糕的价格存储在一个变量中。在文件的顶部将其声明为一个顶级专用常量,该声明在类定义之外(但在 import 语句之后)。使用 const 修饰符,如需将其设为只读,请使用 val
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

回想一下,常量值(在 Kotlin 中使用 const 关键字标记)不会更改,并且该值在编译时已知。如需详细了解常量,请参阅相应的文档

  1. 现在,您已经定义了每个纸杯蛋糕的价格,接下来创建一个辅助方法来计算价格。此方法可以为 private,因为它只在此类中使用。您将在下一项任务中更改价格逻辑以包含当天取货费用。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

这行代码用每个纸杯蛋糕的价格乘以订购的纸杯蛋糕数量。对于圆括号中的代码,由于 quantity.value 的值可以为 null,因此使用 elvis 运算符 (?:)。elvis 运算符 (?:) 表示如果左侧的表达式不为 null,则使用该表达式。否则,如果左侧的表达式为 null,则使用 elvis 运算符右侧的表达式(在本例中为 0)。

  1. 在同一 OrderViewModel 类中,设置数量后更新价格变量。在 setQuantity() 函数中对新函数进行调用。
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

将价格属性绑定到界面

  1. fragment_flavor.xmlfragment_pickup.xmlfragment_summary.xml 的布局中,确保定义了类型为 com.example.cupcake.model.OrderViewModel 的数据变量 viewModel
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. 在每个 fragment 类的 onViewCreated() 方法中,确保将 fragment 中的视图模型对象实例绑定到布局中的视图模型数据变量。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. 在每个 fragment 布局中,如果价格显示在布局中,则使用 viewModel 变量设置价格。首先修改 fragment_flavor.xml 文件。对于 subtotal 文本视图,将 android:text 属性的值设置为 "@{@string/subtotal_price(viewModel.price)}".。此数据绑定布局表达式使用字符串资源 @string/subtotal_price,并传入一个形参,它是来自视图模型的价格,因此输出将显示正确的字符串,例如 Subtotal 12.0
...

<TextView
    android:id="@+id/subtotal"
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

您使用的是 strings.xml 文件中已声明的以下字符串资源:

<string name="subtotal_price">Subtotal %s</string>
  1. 运行应用。如果您在起始 fragment 中选择 One cupcake,口味 fragment 将显示 Subtotal 2.0。如果您选择 Six cupcakes,口味 fragment 将显示 Subtotal 12.0,依此类推。您稍后会将价格的格式设置为适当的货币格式,因此目前此行为符合预期。

  1. 现在,对取货 fragment 和摘要 fragment 进行类似的更改。在 fragment_pickup.xmlfragment_summary.xml 布局中,修改文本视图,使其也使用 viewModel price 属性。

fragment_pickup.xml

...

<TextView
    android:id="@+id/subtotal"
    ...
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

fragment_summary.xml

...

<TextView
   android:id="@+id/total"
   ...
   android:text="@{@string/total_price(viewModel.price)}"
   ... />

...

  1. 运行应用。确保订购数量为 1 个、6 个和 12 个纸杯蛋糕时订单摘要中显示的价格计算正确。前面已经提到,预计目前价格的格式设置不正确($2 将显示为 2.0,$12 将显示为 12.0)。

当天取货收取额外费用

在此任务中,您将实现第二个规则,即如果当天取货,订单价格另加 $3.00。

  1. OrderViewModel 类中,针对当天取货费用定义一个新的顶级专用常量。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. updatePrice() 中,检查用户是否选择了当天取货。检查视图模型中的日期 (_date.value) 是否与 dateOptions 列表中的第一项(始终为当天)相同。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. 为使这些计算更简单,引入一个临时变量 calculatedPrice。计算更新后的价格,并将其赋值回 _price.value
private fun updatePrice() {
    var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    // If the user selected the first option (today) for pickup, add the surcharge
    if (dateOptions[0] == _date.value) {
        calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
    }
    _price.value = calculatedPrice
}
  1. setDate() 方法调用 updatePrice() 辅助方法以添加当天取货费用。
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. 运行应用,并在应用中导航。您会注意到,如果更改取货日期,不会从总价中去掉当天取货费用。这是因为,价格在视图模型中发生了变化,但没有通知绑定布局。

2ea8e000fb4e6ec8.png

设置生命周期所有者以观察 LiveData

LifecycleOwner 是一个具有 Android 生命周期的类,如 activity 或 fragment。仅当生命周期所有者处于活动状态时(STARTEDRESUMED),LiveData 观察器才会观察应用数据的更改。

在应用中,LiveData 对象或可观察的数据是视图模型中的 price 属性。生命周期所有者是口味 fragment、取货 fragment 和摘要 fragment。LiveData 观察器是包含价格之类的可观察数据的布局文件中的绑定表达式。借助数据绑定,当可观察的值发生变化时,它绑定到的界面元素会自动更新。

绑定表达式的示例:android:text="@{@string/subtotal_price(viewModel.price)}"

为使界面元素自动更新,您必须在应用中将 binding.lifecycleOwner

生命周期所有者关联。接下来您将实现此行为。

  1. FlavorFragmentPickupFragmentSummaryFragment 类中的 onViewCreated() 方法内,在 binding?.apply 代码块中添加以下代码。这样会在绑定对象上设置生命周期所有者。通过设置生命周期所有者,应用将能够观察 LiveData 对象。
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. 再次运行应用。在取货屏幕中,更改取货日期,并注意价格自动更改方式的差异。当天取货费用会正确地反映在摘要屏幕中。
  2. 请注意,如果您选择当天取货,订单的价格会增加 $3.00。如果选择在将来的任何日期取货,价格应该仍然是纸杯蛋糕的数量 x $2.00。

  1. 使用不同的纸杯蛋糕数量、口味和取货日期来测试不同的用例。现在,您应该会看到每个 fragment 上视图模型中的价格更新。最好的一点是,您不必编写额外的 Kotlin 代码,即可让界面每次都随价格更新。

f4c0a3c5ea916d03.png

为了完成价格功能的实现,您需要将价格的格式设置为本地货币。

通过 LiveData 转换设置价格的格式

LiveData 转换方法提供了一种方式来对源 LiveData 执行数据操作,并返回生成的 LiveData 对象。简单来说,就是它将 LiveData 的值转换为其他值。除非某个观察器正在观察 LiveData 对象,否则不会计算这些转换。

Transformations.map() 是一个转换函数,此方法将源 LiveData 和一个函数作为参数。该函数可操控源 LiveData,并返回更新后的值(该值也可观察)。

您可以使用 LiveData 转换的一些实时示例如下:

  • 设置要显示的日期和时间字符串的格式
  • 对项列表进行排序
  • 对项进行过滤或分组
  • 从列表中计算结果(如所有项的总和与项数)、返回最后一项,等等。

在此任务中,您将使用 Transformations.map() 方法设置价格的格式以使用本地货币。您会将十进制值 (LiveData<Double>) 形式的原始价格转换为字符串值 (LiveData<String>)。

  1. OrderViewModel 类中,将后备属性类型更改为 LiveData<String>,而不是 LiveData<Double>.。统一格式的价格将是一个带有货币符号(如“$”)的字符串。您将在下一步中修复初始化错误。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. 使用 Transformations.map() 初始化新的变量,并传入 _price 和一个 lambda 函数。使用 NumberFormat 类中的 getCurrencyInstance() 方法将价格转换为本地货币格式。转换代码将如下所示。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

您需要导入 androidx.lifecycle.Transformationsjava.text.NumberFormat

  1. 运行应用。现在,您应该会看到小计和合计的设置了格式的价格字符串。这样用户理解起来就容易多了!

1853bd13a07f1bc7.png

  1. 测试应用是否按预期运行。测试一些用例,如订购 1 个纸杯蛋糕、订购 6 个纸杯蛋糕或订购 12 个纸杯蛋糕。确保价格在每个屏幕上都能正确更新。对于口味 fragment 和取货 fragment,应显示 Subtotal $2.00;对于订单摘要,应显示 Total $2.00。此外,还应确保订单摘要显示正确的订单详情。

9. 使用监听器绑定设置点击监听器

在此任务中,您将使用监听器绑定将 fragment 类中的按钮点击监听器绑定到布局。

  1. 在布局文件 fragment_start.xml 中,添加一个名为 startFragment 且类型为 com.example.cupcake.StartFragment 的数据变量。确保 fragment 的软件包名称与应用的软件包名称相符。
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. StartFragment.ktonViewCreated() 方法中,将新的数据变量绑定到 fragment 实例。您可以使用 this 关键字来访问 fragment 内的 fragment 实例。移除 binding?.apply 代码块以及其中的代码。完成后的方法应如下所示。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. fragment_start.xml 中,使用监听器绑定向按钮的 onClick 属性添加事件监听器,在 startFragment 上对 orderCupcake() 进行调用,并传入纸杯蛋糕的数量。
<Button
    android:id="@+id/order_one_cupcake"
    android:onClick="@{() -> startFragment.orderCupcake(1)}"
    ... />

<Button
    android:id="@+id/order_six_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(6)}"
    ... />

<Button
    android:id="@+id/order_twelve_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(12)}"
    ... />
  1. 运行应用。请注意,起始 fragment 中的按钮点击处理程序按预期运行。
  2. 同样,也在其他布局中添加上面的数据变量,以绑定 fragment 实例 fragment_flavor.xmlfragment_pickup.xmlfragment_summary.xml

fragment_flavor.xml 中:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="flavorFragment"
            type="com.example.cupcake.FlavorFragment" />
    </data>

    <ScrollView ...>

fragment_pickup.xml 中:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="pickupFragment"
            type="com.example.cupcake.PickupFragment" />
    </data>

    <ScrollView ...>

fragment_summary.xml 中:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView ...>
  1. 在其余 fragment 类的 onViewCreated() 方法中,删除用于手动设置按钮上的点击监听器的代码。
  2. onViewCreated() 方法中,将 fragment 数据变量与 fragment 实例绑定。您将在此处以不同的方式使用 this 关键字,因为在 binding?.apply 代码块内,this 关键字是指绑定实例,而不是 fragment 实例。使用 @ 并明确指定 fragment 类名称,例如 this@FlavorFragment。完成后的 onViewCreated() 方法应如下所示:

FlavorFragment 类中的 onViewCreated() 方法应如下所示:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        viewModel = sharedViewModel
        flavorFragment = this@FlavorFragment
    }
}

PickupFragment 类中的 onViewCreated() 方法应如下所示:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       pickupFragment = this@PickupFragment
   }
}

SummaryFragment 类中生成的 onViewCreated() 方法应如下所示:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       summaryFragment = this@SummaryFragment
   }
}
  1. 同样,在其他布局文件中,向按钮的 onClick 属性添加监听器绑定表达式。

fragment_flavor.xml 中:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> flavorFragment.goToNextScreen()}"
    ... />

fragment_pickup.xml 中:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> pickupFragment.goToNextScreen()}"
    ... />

fragment_summary.xml 中:

<Button
    android:id="@+id/send_button"
    android:onClick="@{() -> summaryFragment.sendOrder()}"
    ...>
  1. 运行应用以验证按钮是否仍按预期工作。行为上应该没有明显的变化,但现在您已使用监听器绑定设置了点击监听器!

恭喜您完成了此 Codelab 的学习并构建了 Cupcake 应用!不过,该应用还没有彻底完成。在下一个 Codelab 中,您将添加一个 Cancel 按钮并修改返回堆栈。您还会学习什么是返回堆栈以及其他新概念。到时候见!

10. 解决方案代码

此 Codelab 的解决方案位于如下所示的项目中。请使用 viewmodel 分支拉取或下载该代码。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。

5b0a76c50478a73f.png

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

在 Android Studio 中打开项目

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

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

21f3eec988dcfbe9.png

  1. Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用按预期构建。
  5. Project 工具窗口中浏览项目文件,了解应用的设置方式。

11. 总结

  • ViewModelAndroid 架构组件的一部分,保存在 ViewModel 中的应用数据在配置更改期间会保留。如需将 ViewModel 添加到应用,您可以创建一个新类,并从 ViewModel 类扩展该类。
  • 共享 ViewModel 用于将来自多个 fragment 的应用数据保存在单个 ViewModel 中。应用中的多个 fragment 将根据其 activity 作用域访问共享 ViewModel
  • LifecycleOwner 是一个具有 Android 生命周期的类,如 activity 或 fragment。
  • 仅当生命周期所有者处于活动状态时(STARTEDRESUMED),LiveData 观察器才会观察应用数据的更改。
  • 监听器绑定是在事件(如 onClick 事件)发生时运行的 lambda 表达式。它们类似于方法引用(如 textview.setOnClickListener(clickListener)),但监听器绑定可让您运行任意数据绑定表达式。
  • LiveData 转换方法提供了一种方式来对源 LiveData 执行数据操作,并返回生成的 LiveData 对象。
  • Android 框架提供了一个名为 SimpleDateFormat 的类,它是一个用于以语言区域敏感的方式对日期进行格式设置和解析的类。使用该类可以对日期进行格式设置(日期 → 文本)和解析(文本 → 日期)。

12. 了解更多内容