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 应用。用户可以在纸杯蛋糕订单中选择纸杯蛋糕的数量、口味及其他选项。
下载此 Codelab 的起始代码
此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码将包含您在之前的 Codelab 中已熟悉的代码。
如果您从 GitHub 下载起始代码,那么请注意,项目的文件夹名称为 android-basics-kotlin-cupcake-app-starter
。在 Android Studio 中打开项目时,请选择此文件夹。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
- 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。
- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。
- 在 Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用按预期构建。
- 在 Project 工具窗口中浏览项目文件,了解应用的设置方式。
起始代码演示
- 在 Android Studio 中打开下载的项目。该项目的文件夹名称为
android-basics-kotlin-cupcake-app-starter
。然后,运行应用。 - 浏览文件以了解起始代码。对于布局文件,您可以使用右上角的 Split 选项同时查看布局和 XML 的预览。
- 编译和运行应用时,您会注意到应用不完整。按钮没有太大的用处(除了用于显示
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.kt
、PickupFragment.kt
和SummaryFragment.kt
类主要包含样板代码以及 Next 或 Send Order to Another App 按钮的点击处理程序,点击这些按钮会显示消息框消息。
资源(res 文件夹):
drawable
文件夹包含第一个屏幕的纸杯蛋糕资源,以及启动器图标文件。navigation/nav_graph.xml
包含四个 fragment 目的地(startFragment
、flavorFragment
、pickupFragment
和summaryFragment
),但不包含操作,您稍后将在此 Codelab 中定义操作。values
文件夹包含用于自定义应用主题的颜色、尺寸、字符串、样式和主题。您应该在之前的 Codelab 中已熟悉这些资源类型。
3. 完成导航图
在此任务中,您会将 Cupcake 应用的屏幕连接在一起,并在应用中实现适当的导航。
您还记得我们使用 Navigation 组件需要满足的条件吗?按照此指南中的说明操作,复习一下如何设置项目和应用,以便:
- 添加 Jetpack Navigation 库
- 向 activity 添加
NavHost
- 创建导航图
- 向导航图添加 fragment 目的地
连接导航图中的目的地
- 在 Android Studio 的 Project 窗口中,依次打开 res > navigation > nav_graph.xml 文件。切换到 Design 标签页(如果尚未选择该标签页)。
- 此时将打开 Navigation Editor,以直观呈现应用中的导航图。您应该会看到应用中已存在的四个 fragment。
- 连接导航图中的 fragment 目的地。创建从
startFragment
到flavorFragment
的操作,从flavorFragment
到pickupFragment
的连接,以及从pickupFragment
到summaryFragment
的连接。如果您需要更详细的说明,请按照接下来的几个步骤操作。 - 将光标悬停在 startFragment 上,直到您看到该 fragment 周围的灰色边框,以及出现在该 fragment 右侧边缘中心的灰色圆圈。点击该圆圈并将其拖动到 flavorFragment,然后松开鼠标。
- 这两个 fragment 之间的箭头指示连接成功,这表示您将能够从 startFragment 导航到 flavorFragment。这称为导航操作,您在之前的 Codelab 中已经学过。
- 同样,添加从 flavorFragment 到 pickupFragment 以及从 pickupFragment 到 summaryFragment 的导航操作。当您创建完导航操作后,完成的导航图应如下所示。
- 您创建的三项新操作也应该反映在 Component Tree 窗格中。
- 定义导航图时,您还需要指定起始目的地。目前,您可以看到 startFragment 旁边有一个小房子图标。
这指示 startFragment 将是要在 NavHost
中显示的第一个 fragment。保留此设置作为应用的预期行为。为了方便您日后参考,您可以随时更改起始目的地,方法是右键点击一个 fragment,然后选择菜单选项 Set as Start Destination。
从起始 fragment 导航到口味 fragment
接下来,您将添加相应的代码,以通过点按第一个 fragment 中的按钮从 startFragment 导航到 flavorFragment,而不是显示 Toast
消息。下面是起始 fragment 布局的参考。您将在后续任务中将纸杯蛋糕的数量传递给口味 fragment。
- 在 Project 窗口中,依次打开 app > java > com.example.cupcake > StartFragment Kotlin 文件。
- 在
onViewCreated()
方法中,请注意,在三个按钮上设置了点击监听器。点按每个按钮时,系统会调用orderCupcake()
方法,并将纸杯蛋糕的数量(1 个、6 个或 12 个纸杯蛋糕)作为其形参。
参考代码:
orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
- 在
orderCupcake()
方法中,将用于显示消息框消息的代码替换为用于导航到口味 fragment 的代码。使用findNavController()
方法获取NavController
并对其调用navigate()
,且传入操作 IDR.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)
}
- 添加导入代码
import
androidx.navigation.fragment.findNavController
,或者您也可以从 Android Studio 提供的选项中进行选择。
向口味 fragment 和取货 fragment 添加导航
与前面的任务类似,在此任务中,您将向其他 fragment(即口味 fragment 和取货 fragment)添加导航。
- 依次打开 app > java > com.example.cupcake > FlavorFragment.kt。请注意,在 Next 按钮点击监听器中调用的方法是
goToNextScreen()
方法。 - 在
FlavorFragment.kt
中的goToNextScreen()
方法内,替换用于显示消息框的代码以导航到取货 fragment。使用操作 IDR.id.action_flavorFragment_to_pickupFragment
,并确保此 ID 与nav_graph.xml.
中声明的操作匹配。
fun goToNextScreen() {
findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}
记得添加 import androidx.navigation.fragment.findNavController
代码以导入相应的类。
- 同样,在
PickupFragment.kt
中的goToNextScreen()
方法内,替换现有代码以导航到摘要 fragment。
fun goToNextScreen() {
findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}
导入 androidx.navigation.fragment.findNavController
。
- 运行应用。确保使用按钮能够在屏幕之间导航。每个 fragment 中显示的信息可能不完整,但不必担心,您将在接下来的步骤中以正确的数据填充这些 fragment。
更新应用栏中的标题
当您在应用中导航时,请注意应用栏中的标题。它始终显示为 Cupcake。
如果能够根据当前 fragment 的功能来提供更相关的标题,那么就会带来更好的用户体验。
使用 NavController
为每个 fragment 更改应用栏(又称为操作栏)中的标题,并显示 Up (←) 按钮。
- 在
MainActivity.kt
中,替换onCreate()
方法以设置导航控制器。从NavHostFragment
获取NavController
的实例。 - 对
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)
}
}
- 当 Android Studio 提示时,添加必要的导入代码。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
- 为每个 fragment 设置应用栏标题。打开
navigation/nav_graph.xml
并切换到 Code 标签页。 - 在
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>
- 运行应用。请注意,当您导航到每个 fragment 目的地时,应用栏中的标题会发生变化。另请注意,应用栏中现在显示了 Up 按钮(箭头 ←)。如果您点按该按钮,它不起任何作用。您将在下一个 Codelab 中实现 Up 按钮行为。
4. 创建共享 ViewModel
让我们继续在每个 fragment 中填充正确的数据。您将使用共享 ViewModel
将应用的数据保存在单个 ViewModel
中。应用中的多个 fragment 将根据其 activity 作用域访问共享 ViewModel
。
在大多数正式版应用中,在 fragment 之间共享数据是常见的用例。例如,在此 Codelab 中的最终版 Cupcake 应用中(请注意下面的屏幕截图),用户在第一个屏幕中选择纸杯蛋糕的数量,在第二个屏幕中,系统根据纸杯蛋糕的数量计算并显示价格。同样,口味和取货日期等其他应用数据也用在摘要屏幕中。
通过查看应用功能,您可以推断,将此订单信息存储在单个 ViewModel
中会很有用,该视图模型可以在此 activity 的 fragment 之间共享。回想一下,ViewModel
是 Android 架构组件的一部分。保存在 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)分离开来。根据功能将代码分离到软件包中是一种编码最佳实践。
- 在 Android Studio 的 Project 窗口中,右键点击 com.example.cupcake > New > Package。
- 此时将打开 New Package 对话框,请将软件包命名为
com.example.cupcake.model
。
- 在
model
软件包下创建OrderViewModel
Kotlin 类。在 Project 窗口中,右键点击model
软件包,然后依次选择 New > Kotlin File/Class。在新对话框中,提供文件名OrderViewModel
。
- 在
OrderViewModel.kt
中,更改类签名以从ViewModel
扩展。
import androidx.lifecycle.ViewModel
class OrderViewModel : ViewModel() {
}
- 在
OrderViewModel
类内,将上述属性添加为private
val
。 - 将属性类型更改为
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
- 在
OrderViewModel
类中,添加上述方法。在方法内,分配传入可变属性的参数。 - 由于这些 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
}
- 构建并运行您的应用,以确保没有编译错误。界面中应该还没有明显的变化。
非常棒!现在,您已经有了视图模型的起始代码。随着您在应用中构建出更多功能并意识到类中需要更多属性和方法,您会逐渐向此类添加更多代码。
如果您看到类名称、属性名称或方法名称在 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>()
- 在
StartFragment
类中,以类变量的形式获取对共享视图模型的引用。使用fragment-ktx
库中的by activityViewModels()
Kotlin 属性委托。
private val sharedViewModel: OrderViewModel by activityViewModels()
您可能需要导入下面这些新类:
import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
- 针对
FlavorFragment
、PickupFragment
和SummaryFragment
类重复执行上面的步骤,您将在此 Codelab 的后面几部分中使用此sharedViewModel
实例。 - 返回
StartFragment
类,您现在可以使用视图模型了。在orderCupcake()
方法的开头,调用共享视图模型中的setQuantity()
方法以更新数量,然后再导航到口味 fragment。
fun orderCupcake(quantity: Int) {
sharedViewModel.setQuantity(quantity)
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- 在
OrderViewModel
类中,添加以下方法来检查是否已设置订购的纸杯蛋糕口味。在后面的步骤中,您将在StartFragment
类中使用此方法。
fun hasNoFlavorSet(): Boolean {
return _flavor.value.isNullOrEmpty()
}
- 在
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)
}
- 构建应用以确保没有编译错误。不过,界面中应该没有明显的变化。
6. 将 ViewModel 与数据绑定搭配使用
接下来,您将使用数据绑定将视图模型数据绑定到界面。您还将根据用户在界面中做出的选择来更新共享视图模型。
回顾数据绑定
回想一下,数据绑定库是 Android Jetpack 的一部分。数据绑定使用声明性格式将布局中的界面组件绑定到应用中的数据源。简而言之,数据绑定就是将数据(从代码)绑定到视图 + 视图绑定(将视图绑定到代码)。通过设置这些绑定并让更新自动执行,可帮助您降低在忘记从代码中手动更新界面时出现错误的几率。
根据用户选择更新口味
- 在
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 ...>
...
- 同样,针对
fragment_pickup.xml
和fragment_summary.xml
重复执行上面的步骤以添加viewModel
布局变量。您将在后面几部分中使用此变量。您不需要在fragment_start.xml
中添加此代码,因为此布局不使用共享视图模型。 - 在
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
- 针对
PickupFragment
和SummaryFragment
类中的onViewCreated()
方法重复执行相同的步骤。
binding?.apply {
viewModel = sharedViewModel
...
}
- 在
fragment_flavor.xml
中,根据视图模型中的flavor
值,使用新布局变量viewModel
来设置单选按钮的checked
属性。如果由单选按钮表示的口味与视图模型中保存的口味相同,则将单选按钮显示为选中状态 (checked
= true)。VanillaRadioButton
的选中状态的绑定表达式将如下所示:
@{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)
),但监听器绑定可让您运行任意数据绑定表达式。
- 在
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>
- 运行应用,请注意 Vanilla 选项在口味 fragment 中默认处于选中状态。
太好了!现在,您可以继续构建后面的 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”表示美国。
现在,使用 SimpleDateFormat
和 Locale
确定 Cupcake 应用的可用取货日期。
- 在
OrderViewModel
类中,添加以下名为getPickupOptions()
的函数,以创建并返回取货日期列表。在该方法中,创建一个名为options
的val
变量,并将其初始化为mutableListOf
<String>()
。
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
}
- 使用
SimpleDateFormat
创建格式设置工具字符串,并传递模式字符串"E MMM d"
和语言区域。在模式字符串中,E
代表星期几,它解析为“Tue Dec 10”。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
当 Android Studio 提示时,导入 java.text.SimpleDateFormat
和 java.util.Locale
。
- 获取
Calendar
实例并将其分配给一个新变量。将此变量设为val
。此变量将包含当前日期和时间。此外,还应导入java.util.Calendar
。
val calendar = Calendar.getInstance()
- 构建一个日期列表,从当前日期开始,还有接下来的三个日期。由于您需要 4 个日期选项,因此请重复此代码块 4 次。此
repeat
代码块会设置日期的格式,将其添加到日期选项列表,然后让日历按 1 天递增。
repeat(4) {
options.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 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
}
- 在
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]
(大后天)
- 在
fragment_pickup.xml
中,对于option0
单选按钮,根据视图模型中的date
值,使用新布局变量viewModel
来设置checked
属性。将viewModel.date
属性与dateOptions
列表中的第一个字符串(即当前日期)进行比较。使用equals
函数进行比较,最终的绑定表达式如下所示:
@{viewModel.date.equals(viewModel.dateOptions[0])}
- 对于同一单选按钮,使用监听器绑定向
onClick
属性添加事件监听器。点击此单选按钮选项时,在viewModel
上对setDate()
进行调用,并传入dateOptions[0]
。 - 对于同一单选按钮,将
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]}"
...
/>
- 针对其他单选按钮重复执行上面的步骤,相应地更改
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]}"
... />
- 运行应用,您应该会看到接下来几天作为可用的取货选项。您的屏幕截图会因您的当前日期而异。请注意,默认情况下未选中任何选项。您将在下一步中实现此行为。
- 在
OrderViewModel
类中,创建一个名为resetOrder()
的函数,以重置视图模型中的MutableLiveData
属性。将dateOptions
列表中的当前日期值赋给_date.
value.
。
fun resetOrder() {
_quantity.value = 0
_flavor.value = ""
_date.value = dateOptions[0]
_price.value = 0.0
}
- 向类中添加一个
init
代码块,并从其调用新方法resetOrder()
。
init {
resetOrder()
}
- 从类的属性声明中移除初始值。现在,创建
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
- 再次运行应用,请注意,默认情况下选中了今天的日期。
更新汇总 fragment 以使用视图模型
现在,让我们继续构建最后一个 fragment。订单摘要 fragment 旨在显示订单详情的摘要。在此任务中,您将利用共享视图模型中的所有订单信息,并使用数据绑定更新屏幕上的订单详情。
- 在
fragment_summary.xml
中,确保您已声明视图模型数据变量viewModel
。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 在
SummaryFragment
的onViewCreated()
中,确保已初始化binding.viewModel
。 - 在
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}"
... />
- 运行并测试应用以验证您选择的订单选项是否显示在订单摘要中。
8. 根据订单详情计算价格
看一下此 Codelab 的最终应用屏幕截图,您会注意到,价格确实显示在每个 fragment 中(StartFragment
除外),这样用户在创建订单时就会知道价格。
下面是纸杯蛋糕店关于如何计算价格的规则。
- 每个纸杯蛋糕 $2.00
- 如果当天取货,订单价格另加 $3.00
因此,如果订购了 6 个纸杯蛋糕,那么价格将为 6 个纸杯蛋糕 x $2/个 = $12。如果用户想要当天取货,那么需要额外支付 $3,这样一来,订单总价就为 $15。
更新视图模型中的价格
如需在应用中添加对此功能的支持,请先处理每个纸杯蛋糕的价格,暂时忽略当天取货费用。
- 打开
OrderViewModel.kt
,将每个纸杯蛋糕的价格存储在一个变量中。在文件的顶部将其声明为一个顶级专用常量,该声明在类定义之外(但在 import 语句之后)。使用const
修饰符,如需将其设为只读,请使用val
。
package ...
import ...
private const val PRICE_PER_CUPCAKE = 2.00
class OrderViewModel : ViewModel() {
...
回想一下,常量值(在 Kotlin 中使用 const
关键字标记)不会更改,并且该值在编译时已知。如需详细了解常量,请参阅相应的文档。
- 现在,您已经定义了每个纸杯蛋糕的价格,接下来创建一个辅助方法来计算价格。此方法可以为
private
,因为它只在此类中使用。您将在下一项任务中更改价格逻辑以包含当天取货费用。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}
这行代码用每个纸杯蛋糕的价格乘以订购的纸杯蛋糕数量。对于圆括号中的代码,由于 quantity.value
的值可以为 null,因此使用 elvis 运算符 (?:
)。elvis 运算符 (?:
) 表示如果左侧的表达式不为 null,则使用该表达式。否则,如果左侧的表达式为 null,则使用 elvis 运算符右侧的表达式(在本例中为 0
)。
- 在同一
OrderViewModel
类中,设置数量后更新价格变量。在setQuantity()
函数中对新函数进行调用。
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
updatePrice()
}
将价格属性绑定到界面
- 在
fragment_flavor.xml
、fragment_pickup.xml
和fragment_summary.xml
的布局中,确保定义了类型为com.example.cupcake.model.OrderViewModel
的数据变量viewModel
。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 在每个 fragment 类的
onViewCreated()
方法中,确保将 fragment 中的视图模型对象实例绑定到布局中的视图模型数据变量。
binding?.apply {
viewModel = sharedViewModel
...
}
- 在每个 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>
- 运行应用。如果您在起始 fragment 中选择 One cupcake,口味 fragment 将显示 Subtotal 2.0。如果您选择 Six cupcakes,口味 fragment 将显示 Subtotal 12.0,依此类推。您稍后会将价格的格式设置为适当的货币格式,因此目前此行为符合预期。
- 现在,对取货 fragment 和摘要 fragment 进行类似的更改。在
fragment_pickup.xml
和fragment_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 个、6 个和 12 个纸杯蛋糕时订单摘要中显示的价格计算正确。前面已经提到,预计目前价格的格式设置不正确($2 将显示为 2.0,$12 将显示为 12.0)。
当天取货收取额外费用
在此任务中,您将实现第二个规则,即如果当天取货,订单价格另加 $3.00。
- 在
OrderViewModel
类中,针对当天取货费用定义一个新的顶级专用常量。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
- 在
updatePrice()
中,检查用户是否选择了当天取货。检查视图模型中的日期 (_date.
value
) 是否与dateOptions
列表中的第一项(始终为当天)相同。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
if (dateOptions[0] == _date.value) {
}
}
- 为使这些计算更简单,引入一个临时变量
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
}
- 从
setDate()
方法调用updatePrice()
辅助方法以添加当天取货费用。
fun setDate(pickupDate: String) {
_date.value = pickupDate
updatePrice()
}
- 运行应用,并在应用中导航。您会注意到,如果更改取货日期,不会从总价中去掉当天取货费用。这是因为,价格在视图模型中发生了变化,但没有通知绑定布局。
设置生命周期所有者以观察 LiveData
LifecycleOwner
是一个具有 Android 生命周期的类,如 activity 或 fragment。仅当生命周期所有者处于活动状态时(STARTED
或 RESUMED
),LiveData
观察器才会观察应用数据的更改。
在应用中,LiveData
对象或可观察的数据是视图模型中的 price
属性。生命周期所有者是口味 fragment、取货 fragment 和摘要 fragment。LiveData
观察器是包含价格之类的可观察数据的布局文件中的绑定表达式。借助数据绑定,当可观察的值发生变化时,它绑定到的界面元素会自动更新。
绑定表达式的示例:android:text="@{@string/subtotal_price(viewModel.price)}"
为使界面元素自动更新,您必须在应用中将 binding.
lifecycleOwner
与
生命周期所有者关联。接下来您将实现此行为。
- 在
FlavorFragment
、PickupFragment
和SummaryFragment
类中的onViewCreated()
方法内,在binding?.apply
代码块中添加以下代码。这样会在绑定对象上设置生命周期所有者。通过设置生命周期所有者,应用将能够观察LiveData
对象。
binding?.apply {
lifecycleOwner = viewLifecycleOwner
...
}
- 再次运行应用。在取货屏幕中,更改取货日期,并注意价格自动更改方式的差异。当天取货费用会正确地反映在摘要屏幕中。
- 请注意,如果您选择当天取货,订单的价格会增加 $3.00。如果选择在将来的任何日期取货,价格应该仍然是纸杯蛋糕的数量 x $2.00。
- 使用不同的纸杯蛋糕数量、口味和取货日期来测试不同的用例。现在,您应该会看到每个 fragment 上视图模型中的价格更新。最好的一点是,您不必编写额外的 Kotlin 代码,即可让界面每次都随价格更新。
为了完成价格功能的实现,您需要将价格的格式设置为本地货币。
通过 LiveData 转换设置价格的格式
LiveData
转换方法提供了一种方式来对源 LiveData
执行数据操作,并返回生成的 LiveData
对象。简单来说,就是它将 LiveData
的值转换为其他值。除非某个观察器正在观察 LiveData
对象,否则不会计算这些转换。
Transformations.map()
是一个转换函数,此方法将源 LiveData
和一个函数作为参数。该函数可操控源 LiveData
,并返回更新后的值(该值也可观察)。
您可以使用 LiveData 转换的一些实时示例如下:
- 设置要显示的日期和时间字符串的格式
- 对项列表进行排序
- 对项进行过滤或分组
- 从列表中计算结果(如所有项的总和与项数)、返回最后一项,等等。
在此任务中,您将使用 Transformations.map()
方法设置价格的格式以使用本地货币。您会将十进制值 (LiveData<Double>
) 形式的原始价格转换为字符串值 (LiveData<String>
)。
- 在
OrderViewModel
类中,将后备属性类型更改为LiveData<String>
,而不是LiveData<Double>.
。统一格式的价格将是一个带有货币符号(如“$”)的字符串。您将在下一步中修复初始化错误。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
- 使用
Transformations.map()
初始化新的变量,并传入_price
和一个 lambda 函数。使用NumberFormat
类中的getCurrencyInstance()
方法将价格转换为本地货币格式。转换代码将如下所示。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
NumberFormat.getCurrencyInstance().format(it)
}
您需要导入 androidx.lifecycle.Transformations
和 java.text.NumberFormat
。
- 运行应用。现在,您应该会看到小计和合计的设置了格式的价格字符串。这样用户理解起来就容易多了!
- 测试应用是否按预期运行。测试一些用例,如订购 1 个纸杯蛋糕、订购 6 个纸杯蛋糕或订购 12 个纸杯蛋糕。确保价格在每个屏幕上都能正确更新。对于口味 fragment 和取货 fragment,应显示 Subtotal $2.00;对于订单摘要,应显示 Total $2.00。此外,还应确保订单摘要显示正确的订单详情。
9. 使用监听器绑定设置点击监听器
在此任务中,您将使用监听器绑定将 fragment 类中的按钮点击监听器绑定到布局。
- 在布局文件
fragment_start.xml
中,添加一个名为startFragment
且类型为com.example.cupcake.StartFragment
的数据变量。确保 fragment 的软件包名称与应用的软件包名称相符。
<layout ...>
<data>
<variable
name="startFragment"
type="com.example.cupcake.StartFragment" />
</data>
...
<ScrollView ...>
- 在
StartFragment.kt
的onViewCreated()
方法中,将新的数据变量绑定到 fragment 实例。您可以使用this
关键字来访问 fragment 内的 fragment 实例。移除binding?.
apply
代码块以及其中的代码。完成后的方法应如下所示。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.startFragment = this
}
- 在
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)}"
... />
- 运行应用。请注意,起始 fragment 中的按钮点击处理程序按预期运行。
- 同样,也在其他布局中添加上面的数据变量,以绑定 fragment 实例
fragment_flavor.xml
、fragment_pickup.xml
和fragment_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 ...>
- 在其余 fragment 类的
onViewCreated()
方法中,删除用于手动设置按钮上的点击监听器的代码。 - 在
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
}
}
- 同样,在其他布局文件中,向按钮的
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()}"
...>
- 运行应用以验证按钮是否仍按预期工作。行为上应该没有明显的变化,但现在您已使用监听器绑定设置了点击监听器!
恭喜您完成了此 Codelab 的学习并构建了 Cupcake 应用!不过,该应用还没有彻底完成。在下一个 Codelab 中,您将添加一个 Cancel 按钮并修改返回堆栈。您还会学习什么是返回堆栈以及其他新概念。到时候见!
10. 解决方案代码
此 Codelab 的解决方案位于如下所示的项目中。请使用 viewmodel 分支拉取或下载该代码。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
- 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。
- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。
- 在 Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用按预期构建。
- 在 Project 工具窗口中浏览项目文件,了解应用的设置方式。
11. 总结
ViewModel
是 Android 架构组件的一部分,保存在ViewModel
中的应用数据在配置更改期间会保留。如需将ViewModel
添加到应用,您可以创建一个新类,并从ViewModel
类扩展该类。- 共享
ViewModel
用于将来自多个 fragment 的应用数据保存在单个ViewModel
中。应用中的多个 fragment 将根据其 activity 作用域访问共享ViewModel
。 LifecycleOwner
是一个具有 Android 生命周期的类,如 activity 或 fragment。- 仅当生命周期所有者处于活动状态时(
STARTED
或RESUMED
),LiveData
观察器才会观察应用数据的更改。 - 监听器绑定是在事件(如
onClick
事件)发生时运行的 lambda 表达式。它们类似于方法引用(如textview.setOnClickListener(clickListener)
),但监听器绑定可让您运行任意数据绑定表达式。 LiveData
转换方法提供了一种方式来对源LiveData
执行数据操作,并返回生成的LiveData
对象。- Android 框架提供了一个名为
SimpleDateFormat
的类,它是一个用于以语言区域敏感的方式对日期进行格式设置和解析的类。使用该类可以对日期进行格式设置(日期 → 文本)和解析(文本 → 日期)。