简介
在上一个 Codelab 中,您学习了如何从网络服务中获取数据,以及如何将响应解析为 Kotlin 对象。在本 Codelab 中,您将利用这些知识从一个网址加载和显示照片。此外,您还将回顾如何构建 RecyclerView 以及用它在概览页面上显示图片网格。
前提条件
- 如何创建和使用 fragment。
- 如何从 REST 网络服务中检索 JSON,并使用 Retrofit 和 Moshi 库将该数据解析为 Kotlin 对象。
- 如何使用
RecyclerView构建网格布局。 Adapter、ViewHolder和DiffUtil如何工作。
学习内容
- 如何使用 Coil 库从一个网址加载和显示图片。
- 如何使用
RecyclerView和网格适配器显示图片网格。 - 如何处理图片下载和显示时的潜在错误。
构建内容
- 修改 MarsPhotos 应用,从火星数据中获取图片网址,并使用 Coil 加载和显示该图片。
- 将加载动画和错误图标添加到应用中。
- 使用
RecyclerView显示火星图片网格。 - 将状态和错误处理添加到
RecyclerView。
所需条件
- 一台安装了新版网络浏览器(如最新版 Chrome)的计算机。
- 计算机可以访问互联网。
在此 Codelab 中,您可以继续使用上一个 Codelab 中名为 MarsPhotos 的应用。MarsPhotos 应用会连接到网络服务,以检索并显示使用 Retrofit 检索到的 Kotlin 对象数量。这些 Kotlin 对象包含了 NASA 火星探测器拍摄的火星表面真实照片的网址。
您将在此 Codelab 中构建的应用版本会填充概览页面,该页面将以图片网格形式显示火星照片。这些图片是您的应用从 Mars 网络服务检索到的数据的一部分。您的应用将使用 Coil 库加载和显示图片,并使用 RecyclerView 为图片创建网格布局。您的应用还将妥善处理网络错误。

从一个网址显示照片可能听起来非常简单,但实际上却需要完成大量工程才能正常运行。图片必须下载、在内部存储并从其压缩格式解码为 Android 可使用的图片。应将图片缓存到内存缓存、基于存储空间的缓存或这两种缓存中。所有操作都必须在低优先级的后台线程中进行,以便界面保持快速响应。另外,为获得最佳网络和 CPU 性能,可能需要同时获取和解码多张图片。
幸好,您可以使用名为 Coil 的社区开发的库下载、缓冲、解码以及缓存您的图片。如果不使用 Coil,您会需要执行更多操作。
Coil 基本上需要有以下两项:
- 需要加载和显示的图片的网址。
- 用于实际显示该图片的
ImageView对象。
在此任务中,您将学习如何使用 Coil 显示火星网络服务中的单张图片。您可以在网络服务返回的照片列表中显示第一张火星照片的图片。下面是操作之前和之后的屏幕截图:
|
|
添加 Coil 依赖项
- 打开上一个 Codelab 中的 MarsPhotos 解决方案应用。
- 运行该应用即可查看其功能。(显示检索到的火星照片的总数)。
- 打开 build.gradle (Module: app)。
- 在
dependencies部分中,为 Coil 库添加下面这行代码:
// Coil
implementation "io.coil-kt:coil:1.1.1"
从 Ccee 文档页面查看并更新该库的最新版本。
- Coil 库托管在
mavenCentral()代码库中,并可以通过该代码库使用。在 build.gradle (Project: MarsPhotos) 中,在顶部的repositories代码块中添加mavenCentral()。
repositories {
google()
jcenter()
mavenCentral()
}
- 请点击 Sync Now,以使用新的依赖项重建项目。
更新 ViewModel
在此步骤中,您将向 OverviewViewModel 类添加 LiveData 属性,以存储接收到的 Kotlin 对象,即 MarsPhoto。
- 打开
overview/OverviewViewModel.kt。在_status属性声明的下面,添加一个名为_photos的新可变属性,其类型为MutableLiveData,可存储单个MarsPhoto对象。
private val _photos = MutableLiveData<MarsPhoto>()
根据需要导入 com.example.android.marsphotos.network.MarsPhoto。
- 在
_photos声明的下面,添加一个名为photos的公开后备字段LiveData<MarsPhoto>。
val photos: LiveData<MarsPhoto> = _photos
- 在
getMarsPhotos()方法中的try{}代码块内,找到用于将从网络服务中检索到的数据设置为listResult.的代码行
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- 将检索到的第一张火星照片分配给新变量
_photos。将listResult更改为_photos.value。在索引0处指定前几张照片的网址。这会抛出一个错误,您稍后将进行修复。
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
...
}
- 在下一行代码中,将
status.value更新为以下内容。使用新属性中的数据,而非listResult。显示照片列表中的第一张图片的网址。.
try {
...
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 完整的
try{}代码块现在应如下所示:
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 运行应用。现在,
TextView会显示第一张火星照片的网址。到目前为止,您已为该网址设置ViewModel和LiveData。

使用绑定适配器
绑定适配器是用于为视图的自定义属性创建自定义 setter 的注解方法。
通常,当使用 android:text="Sample Text" 代码在 XML 中设置属性时,Android 系统会自动查找名称与 text 属性相同的 setter,后者由 setText(String: text) 方法设置。setText(String: text) 方法是 Android 框架提供的某些视图的 setter 方法。可以使用绑定适配器自定义类似行为;您可以提供一个自定义数据和自定义逻辑,该逻辑将由数据绑定库调用。
示例:
执行比只是对图片视图调用 setter 方法更为复杂的操作,设置一张可绘制的图片。考虑从互联网加载界面线程(主线程)中的图片。首先,选择一个自定义属性以将图片分配给 ImageView。在以下示例中,为 imageUrl。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"/>
如果您未添加任何其他代码,系统会在 ImageView 中查找 setImageUrl(String) 方法,但不会找到该方法,并且会抛出错误,因为这是框架未提供的自定义属性。您必须创建一个实现方法并将 app:imageUrl 属性设置为 ImageView。您将使用绑定适配器(注解方法)来完成此操作。
绑定适配器示例:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
// Load the image in the background using Coil.
}
}
}
@BindingAdapter 注解会将属性名称作为其参数。
在 bindImage 方法中,第一个方法参数是目标视图类型,第二个参数是要设置为属性的值。
在该方法内,Coil 库从界面线程中加载图片,并将其设置为 ImageView。
创建绑定适配器并使用 Coil
- 在
com.example.android.marsphotos软件包中,创建一个名为BindingAdapters的 Kotlin 文件。此文件将保留您在整个应用中使用的绑定适配器。

- 在
BindingAdapters.kt中,创建一个bindImage()函数,该函数将ImageView和String作为参数。
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
在收到请求时,导入 android.widget.ImageView。
- 使用
@BindingAdapter为该函数添加注解。@BindingAdapter注解用于指示数据绑定,在视图项目具有imageUrl属性时执行此绑定适配器。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
在收到请求时,导入 androidx.databinding.BindingAdapter。
let 作用域函数
let 是 Kotlin 的 Scope 函数之一,让您能够在对象的上下文中执行代码块。Kotlin 中有五个作用域函数,请参阅文档以了解详情。
用法:
let 用于对调用链结果调用一个或多个函数。
let 函数和安全调用运算符 (?.) 用于对对象执行 null 安全操作。在这种情况下,仅当对象不为 null 时,系统才会执行 let 代码块。
- 在
bindImage()函数中,使用安全调用运算符向imageURL参数添加let{}代码块。
imgUrl?.let {
}
- 在
let{}代码块内,添加以下代码行,以使用toUri()方法将网址字符串转换为Uri对象。如需使用 HTTPS 架构,请将buildUpon.scheme("https")附加到toUri构建器。调用build()以构建对象。
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
在收到请求时,导入 androidx.core.net.toUri。
- 在
let{}代码块中,在imgUri声明之后,使用 Coil 中的load(){}将imgUri对象中的图片加载到imgView。
imgView.load(imgUri) {
}
在收到请求时,导入 coil.load。
- 您的完整方法应如下所示:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)
}
}
更新布局和 fragment
在上一部分中,您使用了 Coil 图片库来加载图片。如需在屏幕上显示图片,下一步是使用新属性更新 ImageView,以显示单张图片。
稍后在 Codelab 中,您将使用 res/layout/grid_view_item.xml 作为 RecyclerView 中的每个网格项的布局资源文件。在此任务中,您将暂时使用此文件,用您在上一个任务中检索到的图片网址来显示图片。您暂时使用的是此布局文件,而不是 fragment_overview.xml。
- 打开
res/layout/grid_view_item.xml。 - 在
<ImageView>元素上方,为数据绑定添加<data>元素,并绑定到OverviewViewModel类:
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
- 将
app:imageUrl属性添加到ImageView元素中,以使用新的图片加载绑定适配器。请注意,photos包含从服务器检索到的列表MarsPhotos。为imageUrl属性分配第一个条目网址。
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
... />
- 打开
overview/OverviewFragment.kt。在onCreateView()方法中,注解掉扩充FragmentOverviewBinding类的代码行并将它分配给绑定变量。您将看到由于删除此代码行而出现的错误。这只是临时错误,您稍后将对这些错误进行修复。
//val binding = FragmentOverviewBinding.inflate(inflater)
- 请使用
grid_view_item.xml,而不是fragment_overview.xml.。添加以下代码行,以扩充GridViewItemBinding类。
val binding = GridViewItemBinding.inflate(inflater)
如果收到请求,则导入 com.example.android.marsphotos. databinding.GridViewItemBinding。
- 请运行应用。现在,您应该会看到一张火星图片。

添加加载和错误图片
使用 Coil 时,系统会在加载图片时显示占位符图片,并在加载失败时显示错误消息(例如,图片丢失或损坏),从而可以改善用户体验。在此步骤中,您需要将该功能添加到绑定适配器中。
- 打开
res/drawable/ic_broken_image.xml,然后点击右侧的 Design 标签页。对于错误图片,您将使用内置图标库中提供的损坏图片图标。此矢量可绘制对象使用android:tint属性将图标设为灰色。

- 打开
res/drawable/loading_animation.xml。该可绘制对象是围绕中心点旋转图片可绘制对象loading_img.xml的动画。(您在预览中看不到这段动画。)

- 返回
BindingAdapters.kt文件。在bindImage()方法中,更新imgView.load(imgUri)的调用以添加尾部 lambda,如下所示:此代码可设置加载时要使用的占位符加载图片(loading_animation可绘制对象)。此代码还可设置图片加载失败时要使用的图片(broken_image可绘制对象)。
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
- 完整的
bindImage()方法现在看上去会像下面这样:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
- 请运行应用。根据网络连接速度,您可能会短暂地看到加载图片显示为 Glide 下载内容,并显示属性图片。但是您不会看到损坏图片图标,即使您关闭网络也是如此 - 您将在 Codelab 的最后一个任务中修复该问题。

- 还原您在
overview/OverviewFragment.kt中进行的临时更改。在方法onCreateview()中,取消备注扩充FragmentOverviewBinding的代码行。删除或注解掉扩充GridViewIteMBinding的代码行。
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
您的应用现在从互联网加载了一张火星照片。您使用前 MarsPhoto 个列表项的数据在 ViewModel 中创建了一个 LiveData 属性,并使用该火星照片数据中的图片网址填充了 ImageView。但是,应用的目标是显示图片网格,因此,在此任务中,您将使用带网格布局管理器的 RecyclerView 来显示图片网格。
更新 ViewModel
在上一个任务中,您在 OverviewViewModel 中添加了一个名为 _photos 的 LiveData 对象,用于保存一个 MarsPhoto 对象,即来自网络服务的响应列表中的第一个对象。在这一步中,您将更改此 LiveData 以保存 MarsPhoto 对象的完整列表。
- 打开
overview/OverviewViewModel.kt。 - 将
_photos类型更改为MarsPhoto对象的列表。
private val _photos = MutableLiveData<List<MarsPhoto>>()
- 将后备属性
photos类型也替换为List<MarsPhoto>类型:
val photos: LiveData<List<MarsPhoto>> = _photos
- 向下滚动到
getMarsPhotos()方法中的try {}代码块。MarsApi.retrofitService.getPhotos()将
返回一个 MarsPhoto 对象列表,您可以只将该列表分配给 _photos.value。
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
- 完整的
try/catch代码块现在应如下所示:
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
网格布局
RecyclerView 的 GridLayoutManager 将数据布局为可滚动的网格,如下所示。

从设计角度来看,网格布局最适合可以表示为图标或图片的列表,例如火星照片浏览应用中的列表。
网格布局如何布局列表项
网格布局将以行和列的网格安排列表项。假定您采用的是垂直滚动方式,默认情况下,一行中的每个列表项会占据一个“span”。一个列表项可以占据多个 span。在下面的案例中,一个 span 相当于列宽为 3。
在下面的两个示例中,每一行由三个 span 组成。默认情况下,GridLayoutManager 会在一个 span 中布局每个列表项,直到达到您指定的 span 计数为止。达到 span 计数时,它会换行至下一行。
|
|
添加 Recyclerview
在此步骤中,您将更改应用布局,使用带有网格布局的 Recycler 视图,而非单个图片视图。
- 打开
layout/gridview_item.xml。移除viewModel数据变量。 - 在
<data>标签内,添加类型为MarsPhoto的以下photo变量。
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
- 在
<ImageView>中,更改app:imageUrl属性以引用MarsPhoto对象中的图片网址。这些更改将撤消您在上一个任务中所做的临时更改。
app:imageUrl="@{photo.imgSrcUrl}"
- 打开
layout/fragment_overview.xml。删除整个<TextView>元素。 - 改为添加以下
<RecyclerView>元素。将 ID 设置为photos_grid,将width和height属性设置为0dp,因此它会填充父级ConstraintLayout。您将使用网格布局,因此请将layoutManager属性设置为androidx.recyclerview.widget.GridLayoutManager。将spanCount设置为2,您将得到两列。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2" />
- 如需预览上述代码在 Design 视图中的显示效果,请使用
tools:itemCount将布局中显示的项数设置为16。itemCount属性指定了布局编辑器应该在 Preview 窗口中呈现的项数。使用tools:listitem将列表项的布局设置为grid_view_item。
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
- 切换到 Design 视图,您应该会看到类似如下屏幕截图的预览。这看起来不是火星照片,但是将显示您的 Recycler 视图网格布局看起来会是什么样。预览为
recyclerview的每个网格项使用内边距和grid_view_item布局。

- 根据 Material Design 准则,列表顶部、底部和侧边之间应留出
8dp的空间,而列表项之间应留出4dp的空间。您可以通过结合使用fragment_overview.xml布局和gridview_item.xml布局中的内边距来实现此目的。

- 打开
layout/gridview_item.xml。请注意padding属性,列表项的外侧和内容之间已有2dp的内边距。这样的话,我们在列表项内容之间就会有4dp的空间,而外边缘会有2dp的空间,这意味着我们需要对外边缘再添加6dp内边距,这样才符合设计准则。 - 返回
layout/fragment_overview.xml。为RecyclerView添加6dp的内边距,这样一来,作为准则,外侧将有8dp空间,内侧将有4dp空间。
<androidx.recyclerview.widget.RecyclerView
...
android:padding="6dp"
... />
- 完整的
<RecyclerView>元素应如下所示。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
添加照片网格适配器
现在,fragment_overview 布局具有包含网格布局的 RecyclerView。在此步骤中,您需要通过 RecyclerView 适配器将从网络服务器检索的数据绑定到 RecyclerView。
ListAdapter(刷新程序)
ListAdapter 是 RecyclerView.Adapter 类的子类,用于在 RecyclerView 中显示列表数据,包括后台线程上列表之间的计算差异。
在此应用中,您将在 ListAdapter. 中使用 DiffUtil 实现。使用 DiffUtil 的优势在于,每次添加、移除或更改 RecyclerView 中的某个列表项时,不会刷新整个列表。系统只会刷新已更改的列表项。
将 ListAdapter 添加到您的应用。
- 在
overview软件包中,创建一个名为PhotoGridAdapter.kt的 Kotlin 类。 - 使用如下所示的构造函数参数扩展
ListAdapter中的PhotoGridAdapter类。PhotoGridAdapter类扩展了ListAdapter,其构造函数需要列表项类型、视图容器以及DiffUtil.ItemCallback实现。
class PhotoGridAdapter : ListAdapter<MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
如果收到请求,则导入 androidx.recyclerview.widget.ListAdapter 和 com.example.android.marsphoto.network.MarsPhoto 类。在下面的步骤中,您将实现该构造函数的其他缺失实现,这些实现会产生错误。
- 要解决上述错误,您需要在此步骤中添加所需的方法,并在此任务的稍后过程中实现。点击
PhotoGridAdapter类,点击红色灯泡,然后从下拉菜单中选择实现成员。在弹出式窗口中,选择ListAdapter方法,即onCreateViewHolder()和onBindViewHolder()。Android Studio 仍会显示错误,您将在此任务结束时修复这些错误。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPhotoViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPhotoViewHolder, position: Int) {
TODO("Not yet implemented")
}
要实现 onCreateViewHolder 和 onBindViewHolder 方法,需要添加 MarsPhotoViewHolder,您将在下一步中添加该代码。
- 在
PhotoGridAdapter中,为MarsPhotoViewHolder添加内部类定义,该定义可扩展RecyclerView.ViewHolder。您需要使用GridViewItemBinding变量将MarsPhoto绑定到布局,以便将变量传递到MarsPhotoViewHolder。基础ViewHolder类需要其构造函数中的一个视图,您需要向其传递绑定根视图。
class MarsPhotoViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
如果收到请求,则导入 androidx.recyclerview.widget.RecyclerView 和 com.example.android.marsrealestate.databinding.GridViewItemBinding。
- 在
MarsPhotoViewHolder中,创建一个bind()方法,该方法获取MarsPhoto对象作为参数,并将binding.property设置为该对象。设置属性后,请调用executePendingBindings(),这会导致更新立即执行。
fun bind(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
binding.executePendingBindings()
}
- 依然在
onCreateViewHolder()的PhotoGridAdapter类中,移除 TODO 并添加如下所示的代码行。onCreateViewHolder()方法需要返回新的MarsPhotoViewHolder,方法是扩充GridViewItemBinding并使用父级ViewGroup上下文中的LayoutInflater。
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
如果收到请求,则导入 android.view.LayoutInflater。
- 在
onBindViewHolder()方法中,移除 TODO 并添加如下所示的代码行。在这里,您将调用getItem()以获取与当前RecyclerView位置关联的MarsPhoto对象,然后将该属性传递给MarsPhotoViewHolder中的bind()方法。
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
- 在
PhotoGridAdapter中,为DiffCallback添加伴生对象定义,如下所示。
DiffCallback对象使用您想要比较的泛型对象类型MarsPhoto来扩展DiffUtil.ItemCallback。您将在此实现中比较两个火星照片对象。
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}
在收到请求时,导入 androidx.recyclerview.widget.DiffUtil。
- 按住红色灯泡,为
DiffCallback对象实现比较条件方法,即areItemsTheSame()和areContentsTheSame()。
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented") }
- 在
areItemsTheSame()方法中,移除TODO。DiffUtil将调用此方法来确定两个对象是否代表相同的列表项。DiffUtil使用此方法来确定新的MarsPhoto对象是否与旧的MarsPhoto对象相同。每个列表项(MarsPhoto对象)的 ID 都是唯一的。比较oldItem和newItem的 ID,然后返回结果。
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
- 在
areContentsTheSame()中,移除TODO。DiffUtil会在需要检查两个列表项的数据是否相同时调用此方法。MarsPhoto 中的重要数据是图片网址。比较oldItem和newItem的网址,然后返回结果。
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
请确保您能够编译和运行应用,不会出现任何错误,但模拟器会显示空白屏幕。您已经准备好了 Recycler 视图,但没有向它传递任何数据。您将在下一步中实现此操作。
添加绑定适配器并连接各部分
在此步骤中,您将使用 BindingAdapter 通过 MarsPhoto 对象列表初始化 PhotoGridAdapter。使用 BindingAdapter 设置 RecyclerView 数据会导致数据绑定自动观察 MarsPhoto 对象列表的 LiveData。然后,当 MarsPhoto 列表发生更改时,系统会自动调用绑定适配器。
- 打开
BindingAdapters.kt。 - 在文件末尾添加
bindRecyclerView()方法,该方法会获取RecyclerView和MarsPhoto对象列表作为参数。使用带有listData属性的@BindingAdapter为该方法添加注解。
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
如果收到请求,则导入 androidx.recyclerview.widget.RecyclerView 和 com.example.android.marsphotos.network.MarsPhoto。
- 在
bindRecyclerView()函数中,将recyclerView.adapter类型转换为PhotoGridAdapter并将其分配给新的val属性adapter.。
val adapter = recyclerView.adapter as PhotoGridAdapter
- 在
bindRecyclerView()函数末尾,使用火星照片列表数据调用adapter.submitList()。此代码会在有新列表可供使用时告知RecyclerView。
adapter.submitList(data)
如果收到请求,则导入 com.example.android.marsrealestate.overview.PhotoGridAdapter。
- 完整的
bindRecyclerView绑定适配器应如下所示:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- 如需将所有内容关联在一起,请打开
res/layout/fragment_overview.xml。向RecyclerView元素中添加app:listData属性,并使用数据绑定将该属性设置为viewmodel.photos。这和在上一个任务中针对ImageView执行的操作类似。
app:listData="@{viewModel.photos}"
- 打开
overview/OverviewFragment.kt。在onCreateView()中的return语句前面,将binding.photosGrid中的RecyclerView适配器初始化为新的PhotoGridAdapter对象。
binding.photosGrid.adapter = PhotoGridAdapter()
- 请运行应用。您应该会看到一个包含滚动的火星照片的网格。在您滚动查看新图片时,这个网格看起来有点奇怪。在您滚动时,
RecyclerView的顶部和底部始终会显示内边距,因此它看起来并不像是在操作栏下方滚动的列表。

- 要修复此错误,需要告知
RecyclerView不要使用 android:clipToPadding 属性将内部内容裁剪到内边距。这样会使它在内边距区域绘制滚动视图。返回layout/fragment_overview.xml。为RecyclerView添加android:clipToPadding属性,并将该属性设置为false。
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- 运行您的应用。请注意,应用会像预期的那样,在显示图片本身之前先显示加载进度图标。这是您传递给 Coil 图片库的占位符加载图片。

- 在应用运行时,开启飞行模式。在模拟器中滚动图片。尚未加载的图片显示为损坏图片图标。这是您传递给 Coil 图片库,以便在出现任何网络错误或无法获取图片时显示的图片可绘制对象。

恭喜,您即将大功告成!在下一个也就是最后一个任务中,您将通过向应用添加更多错误处理属性来进一步改善用户体验。
MarsPhotos 应用会在无法获取图片时显示损坏图片图标。但在没有网络连接时,应用会显示空白屏幕。您将在下一步中对空白屏幕进行验证。
- 在设备或模拟器上,开启飞行模式。从 Android Studio 运行应用。请注意空白屏幕。

这样的用户体验并不是很好。在此任务中,您将添加一个基础错误处理属性,以便用户可以清楚地了解所发生的情况。如果互联网不可用,应用将显示连接错误图标,而在应用获取 MarsPhoto 列表时,应用将显示加载动画。
向 ViewModel 添加状态
在此任务中,您将在 OverviewViewModel 中创建一个属性来表示网络请求的状态。需要考虑以下三种状态:正在加载、成功和失败。等待数据时,出现加载状态。在我们成功从网络服务中检索数据时,出现成功状态。失败状态则表示任何网络或连接错误。
枚举 Kotlin 中的类
如需表示应用中的这三种状态,您将使用 enum。enum 是枚举的缩写,意思是集合中所有项的有序列表。每个 enum 常量都是 enum 类的对象。
在 Kotlin 中,enum 是一种数据类型,可以保留一组常量。可以通过在类定义前添加关键字 enum 来定义这些常量,如下所示。枚举常量用逗号分隔开。
定义:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
用法:
var direction = Direction.NORTH;
如上所示,可以使用类名称后跟点 (.) 运算符以及常量名称来引用 enum 对象。
使用 ViewModel 中的状态值添加枚举类定义。
- 打开
overview/OverviewViewModel.kt。在文件的顶部(导入后,在类定义前),添加表示所有可用状态的enum:
enum class MarsApiStatus { LOADING, ERROR, DONE }
- 滚动到
_status和status属性的定义,将类型从String更改为您在上一步中定义的枚举类MarsApiStatus. MarsApiStatus。
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
- 在
getMarsPhotos()方法中,将"Success: ..."字符串更改为MarsApiStatus.DONE状态,将"Failure..."字符串更改为MarsApiStatus.ERROR。
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception)
_status.value = MarsApiStatus.ERROR
}
- 将状态设置为
try {}代码块上方的MarsApiStatus.LOADING。这是协程运行以及等待数据期间显示的初始状态。完整的viewModelScope.launch{}代码块现在应如下所示:
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
}
- 在
catch {}代码块中的错误状态后,将_photos设置为空列表。这会清除 Recycler 视图。
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
- 完整的
getMarsPhotos()方法应如下所示:
private fun getMarsPhotos() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
}
}
您定义了状态的枚举状态,设置了协程开始运行时的加载状态,设置了应用结束从网络服务器检索数据时的完成状态,并设置了出现异常时显示的错误。在下一个任务中,您将使用绑定适配器显示相应的图标。
为状态 ImageView 添加绑定适配器
您已使用一组 enum 状态在 OverviewViewModel 中设置了 MarsApiStatus。在此步骤中,您将使该状态显示在应用中。您可以为 ImageView 使用绑定适配器,以显示加载和错误状态的图标。当应用处于加载状态或错误状态时,ImageView 应可见。应用完成加载后,ImageView 不得处于不可见状态。
- 打开
BindingAdapters.kt,滚动到文件末尾以添加其他适配器。添加一个名为bindStatus()的新绑定适配器,该适配器获取ImageView和MarsApiStatus值作为参数。使用@BindingAdapter为该方法添加注解,并传入自定义属性marsApiStatus作为参数。
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
如果收到请求,则导入 com.example.android.marsrealestate.overview.MarsApiStatus。
- 在
bindStatus()方法中添加一个when {}代码块,以在不同状态之间切换。
when (status) {
}
- 在
when {}中,为加载状态 (MarsApiStatus.LOADING) 添加一个用例。对于此状态,请将ImageView设为可见,然后为其分配加载动画。这与您在上一个任务中用于 Coil 的动画可绘制对象相同。
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
如果收到请求,则导入 android.view.View。
- 为错误状态添加一个用例,
MarsApiStatus.ERROR。与针对LOADING状态的操作类似,将状态ImageView设置为可见,并使用连接错误可绘制对象。
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- 为完成状态添加一个用例,
MarsApiStatus.DONE。在这里,假设您获得成功响应,因此将状态ImageView的可见性设置为View.GONE可隐藏该状态。
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
您已为状态图片视图设置绑定适配器,在下一步中,您将添加使用新绑定适配器的图片视图。
添加状态 ImageView
在此步骤中,您将在 fragment_overview.xml 中添加图片视图,该视图会显示您之前定义的状态。
- 打开
res/layout/fragment_overview.xml。在ConstraintLayout中的RecyclerView元素下,添加如下所示的ImageView。
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
上面的 ImageView 与 RecyclerView 的限制相同。不过,宽度和高度使用 wrap_content 使图片居中,而不是拉伸图片填满视图。另请注意,app:marsApiStatus 属性被设为 viewModel.status,此属性会在 ViewModel 中的状态属性发生更改时调用 BindingAdapter。
- 如需测试上述代码,请在模拟器或设备中开启飞行模式来模拟网络连接错误。编译并运行应用,并注意显示的错误图片:

- 点按“返回”按钮以关闭应用,然后关闭飞行模式。使用“最近”屏幕返回应用。根据网络连接速度,如果应用在开始加载图片之前查询网络服务,您可能会看到一个非常简单的旋转图标。
恭喜您完成此 Codelab 的学习并构建了 MarsPhotos 应用!现在您可以向亲朋好友展示您的应用,其中包含了许多真实的火星照片。
此 Codelab 的解决方案位于如下所示的项目中。请使用 main 分支拉取或下载该代码。
如需获取此 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 开发者文档:
其他:



