简介
在上一个 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 开发者文档:
其他: