Android Kotlin 基础知识:从互联网加载和显示图片

1. 欢迎

此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。

简介

在上一个 Codelab 中,您学习了如何从网络服务中获取数据,以及如何将响应解析为数据对象。在本 Codelab 中,您将利用这些知识从一个网址加载和显示照片。此外,您还将回顾如何构建 RecyclerView 以及用它在概览页面上显示图片网格。

您应当已掌握的内容

  • 如何创建和使用 fragment。
  • 如何使用架构组件,包括视图模型、视图模型工厂、转换和 LiveData
  • 如何从 REST 网络服务中检索 JSON,并使用 RetrofitMoshi 库将该数据解析为 Kotlin 对象。
  • 如何使用 RecyclerView 构建网格布局。
  • AdapterViewHolderDiffUtil 如何工作。

学习内容

  • 如何使用 Glide 库从一个网址加载和显示图片。
  • 如何使用 RecyclerView 和网格适配器显示图片网格。
  • 如何处理图片下载和显示时的潜在错误。

实践内容

  • 修改 MarsRealEstate 应用,从火星资源数据中获取图片网址,并使用 Glide 加载和显示该图片。
  • 将加载动画和错误图标添加到应用中。
  • 使用 RecyclerView 显示火星资源图片网格。
  • 将状态和错误处理添加到 RecyclerView

2. 应用概览

在此 Codelab 中,您将使用一款名为 MarsRealEstate 的应用,该应用会显示火星上的待售资源。该应用需要连接到互联网服务器才能检索和显示资源数据,包括价格以及资源是可出售还是可租赁等详细信息。代表各项资源的图片是由 NASA 的火星探测器拍摄的真实照片。

b31d2df6e0f5e858.png

您在此 Codelab 中构建的应用版本会填充概览页面,该页面将显示图片网格。这些图片是您的应用从火星地产网络服务获取的资源数据的一部分。您的应用将使用 Glide 库加载和显示图片,使用 RecyclerView 为图片创建网格布局,还将妥善处理网络连接错误。

3.任务:显示互联网图片

从一个网址显示照片可能听起来非常简单,但实际上却需要完成大量工程才能正常运行。图片必须下载、在内部存储并从其压缩格式解码为 Android 可使用的图片。应将图片缓存到内存缓存和/或基于存储空间的缓存中。所有操作都必须在低优先级的后台线程中进行,以便界面保持快速响应。另外,为获得最佳网络和 CPU 性能,可能需要同时获取和解码多张图片。如何高效地从网络加载图片,这本身就可以作为一个 Codelab 来学习。

幸好,您可以使用 Glide 这个由社区开发的库来下载、缓冲、解码以及缓存图片。如果不使用 Glide,您将需要执行更多操作。

一般来说,Glide 需要以下两项内容:

  • 需要加载和显示的图片的网址。
  • 用于实际显示该图片的 ImageView 对象。

在此任务中,您将学习如何使用 Glide 显示地产网络服务中的单张图片。您可以在网络服务返回的资源列表中显示代表第一项火星资源的图片。下面是操作之前和之后的屏幕截图:

613d3568c44757d7.png 144df3c58ea3ce44.png

第 1 步:添加 Glide 依赖项

  1. 打开上一个 Codelab 中的 MarsRealEstate 应用。(如果您没有该应用,可以在此处下载 MarsRealEstateNetwork。)
  2. 运行应用以查看其功能(它会显示检索到的火星资源的总数)。
  3. 打开 build.gradle (Module: app)
  4. dependencies 部分中,为 Glide 库添加下面这行代码:
implementation "com.github.bumptech.glide:glide:$version_glide"

请注意,版本号已在项目 Gradle 文件中单独定义。

  1. 请点击 Sync Now,以使用新的依赖项重建项目。

第 2 步:更新视图模型

接下来,您需要更新 OverviewViewModel 类,为单项火星资源添加实时数据。

  1. 打开 overview/OverviewViewModel.kt。在 _responseLiveData 的正下方,为单个 MarsProperty 对象添加内部(可变)和外部(不可变)实时数据。

在收到请求时,导入 MarsProperty 类 (com.example.android.marsrealestate.network.MarsProperty)。

private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. getMarsRealEstateProperties() 方法中的 try/catch {} 代码块内,找到用于将 _response.value 设置为资源数的代码行。在 try/catch 之后添加如下所示的测试。如果 MarsProperty 对象可用,此测试会将 _property LiveData 的值设为 listResult 中的第一项资源。
if (listResult.size > 0) {
    _property.value = listResult[0]
}

完整的 try/catch {} 代码块现在应如下所示:

try {
   val listResult = MarsApi.retrofitService.getProperties()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
    if (listResult.size > 0) {
       _property.value = listResult[0]
   }
} catch (e: Exception) {
  _response.value = "Failure: ${e.message}"
}
  1. 打开 res/layout/fragment_overview.xml 文件。在 <TextView> 元素中,更改 android:text 以绑定到 property LiveDataimgSrcUrl 组件:
android:text="@{viewModel.property.imgSrcUrl}"
  1. 运行应用。TextView 仅显示第一项火星资源图片的网址。到目前为止,您已为该网址设置视图模型和实时数据。

f36bebfbdb68ecb3.png

第 3 步:创建绑定适配器并调用 Glide

现在,您已获得要显示的图片的网址,接下来可以开始使用 Glide 加载该图片了。在此步骤中,您将使用绑定适配器从与 ImageView 关联的 XML 属性中获取网址,并使用 Glide 通过相应网址加载图片。绑定适配器是位于视图与绑定数据之间的扩展方法,可以在数据发生更改时提供自定义行为。在这种情况下,自定义行为是指调用 Glide 将来自网址的图片加载到 ImageView 中。

  1. 打开 BindingAdapters.kt。此文件将保留您在整个应用中使用的绑定适配器。
  2. 创建一个 bindImage() 函数,该函数将 ImageViewString 作为参数。使用 @BindingAdapter 为该函数添加注解。@BindingAdapter 注解用于指示数据绑定,您需要在 XML 项具有 imageUrl 属性时执行此绑定适配器。

在收到请求时,导入 androidx.databinding.BindingAdapterandroid.widget.ImageView

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. bindImage() 函数中,向 imgUrl 参数添加 let {} 代码块:
imgUrl?.let {
}
  1. let {} 代码块内,添加如下所示的代码行,以将网址字符串(来自 XML)转换为 Uri 对象。在收到请求时,导入 androidx.core.net.toUri

您需要让最终的 Uri 对象使用 HTTPS 架构,因为您从中拉取映像的服务器要求使用该架构。如需使用 HTTPS 架构,请将 buildUpon.scheme("https") 附加到 toUri 构建器。toUri() 方法是 Android KTX 核心库中的一个 Kotlin 扩展函数,因此它看起来像是 String 类的一部分。

val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. let {} 内,继续调用 Glide.with(),以将图片从 Uri 对象加载到 ImageView 中。在收到请求时,导入 com.bumptech.glide.Glide
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

您可能需要将以下选项添加到模块 build.gradle 文件中:

android {
...
   kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
   }
...
}

第 4 步:更新布局和 fragment

虽然 Glide 已加载图片,但目前还没显示任何内容。下一步是使用 ImageView 更新布局和 fragment,以显示图片。

  1. 打开 res/layout/gridview_item.xml。这个布局资源文件稍后将用于此 Codelab 中的 RecyclerView 内的每一项。在这一步中,您将暂时使用此文件来仅显示单张图片。
  2. <ImageView> 元素上方,为数据绑定添加 <data> 元素,并绑定到 OverviewViewModel 类:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. app:imageUrl 属性添加到 ImageView 元素中,以使用新的图片加载绑定适配器:
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. 打开 overview/OverviewFragment.kt。在 onCreateView() 方法中,注释掉膨胀了 FragmentOverviewBinding 类的代码行并将它分配给绑定变量。您将看到由于删除此代码行而出现的错误。这只是临时错误,您稍后将处理此错误。
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. 添加一行代码以膨胀 GridViewItemBinding 类。如果收到请求,则导入 com.example.android.marsrealestate. databinding.GridViewItemBinding
val binding = GridViewItemBinding.inflate(inflater)
  1. 请运行应用。现在,您应该会在结果列表中看到第一项 MarsProperty 的图片。
    144df3c58ea3ce44.png

第 5 步:添加简单的加载和错误图片

Glide 会在加载图片时显示占位符图片,并在加载失败时显示错误消息(例如,图片丢失或损坏),从而提升用户体验。在此步骤中,您需要将该功能添加到绑定适配器和布局中。

  1. 打开 res/drawable/ic_broken_image.xml,然后点击右侧的 Design 标签页。对于错误图片,您将使用内置图标库中提供的损坏图片图标。此矢量可绘制对象使用 android:tint 属性将图标设为灰色。

467c213c859e1904.png

  1. 打开 res/drawable/loading_animation.xml。这个可绘制对象是使用 <animated-rotate> 标记定义的动画。该动画会围绕中心点旋转图片可绘制对象 loading_img.xml。(您在预览中看不到这段动画。)

6c1f87d1c932c762.png

  1. 返回 BindingAdapters.kt 文件。在 bindImage() 方法中,更新对 Glide.with() 的调用,以在 load()into() 之间调用 apply() 函数。在收到请求时,导入 com.bumptech.glide.request.RequestOptions

此代码可设置加载时要使用的占位符加载图片(loading_animation 可绘制对象)。此代码还可设置图片加载失败时要使用的图片(broken_image 可绘制对象)。完整的 bindImage() 方法现在应如下所示:

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri =
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  1. 请运行应用。根据网络连接速度,您可能会短暂地看到加载图片显示为 Glide 下载内容,并显示资源图片。但是您不会看到损坏图片图标,即使您关闭网络也是如此。您将在 Codelab 的最后一个任务中修复该错误。

4.任务:使用 RecyclerView 显示图片网格

您的应用现在从互联网加载了火星资源信息。您使用第一个 MarsProperty 列表项的数据在视图模型中创建了一个 LiveData 属性,并使用该资源数据中的图片网址填充了 ImageView。但是,应用的目标是显示图片网格,因此,您需要将 RecyclerViewGridLayoutManager 结合使用。

第 1 步:更新视图模型

现在,视图模型有一个 _property LiveData,用于保存一个 MarsProperty 对象,即网络服务的响应列表中的第一个对象。在此步骤中,您将更改该 LiveData,以保存 MarsProperty 对象的完整列表。

  1. 打开 overview/OverviewViewModel.kt
  2. 将不公开的 _property 变量更改为 _properties。将类型更改为 MarsProperty 对象的列表。
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. 将外部 property 实时数据替换为 properties。也将列表添加为此处的 LiveData 类型:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. 向下滚动到 getMarsRealEstateProperties() 方法。在 try {} 代码块内,将您在上一个任务中添加的完整测试替换为如下所示的代码行。由于 MarsApi.retrofitService.getProperties()

会返回一个 MarsProperty 对象列表,您可以直接将该列表分配给 _properties.value,而不是测试是否成功响应。

_properties.value = MarsApi.retrofitService.getProperties()

完整的 try/catch 代码块现在应如下所示:

try {
    _properties.value = MarsApi.retrofitService.getProperties()
    _response.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

第 2 步:更新布局和 fragment

下一步是更改应用的布局和 fragment,以使用 Recycler 视图和网格布局,而非单个图片视图。

  1. 打开 res/layout/gridview_item.xml。将数据绑定从 OverviewViewModel 更改为 MarsProperty,并将该变量重命名为 "property"
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. <ImageView> 中,更改 app:imageUrl 属性以引用 MarsProperty 对象中的图片网址:
app:imageUrl="@{property.imgSrcUrl}"
  1. 打开 overview/OverviewFragment.kt。在 onCreateview() 中,取消注释膨胀了 FragmentOverviewBinding 的代码行。删除或注释掉膨胀了 GridViewBinding 的代码行。这些更改将撤消您在上一个任务中所做的临时更改。
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. 打开 res/layout/fragment_overview.xml。删除整个 <TextView> 元素。
  2. 改为添加以下 <RecyclerView> 元素,该元素对单个项使用 GridLayoutManagergrid_view_item 布局:
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            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" />

第 3 步:添加照片网格适配器

现在,fragment_overview 布局具有 RecyclerView,而 grid_view_item 布局具有单个 ImageView。在此步骤中,您将通过 RecyclerView 适配器将数据绑定到 RecyclerView

  1. 打开 overview/PhotoGridAdapter.kt
  2. 使用如下所示的构造函数参数创建 PhotoGridAdapter 类。PhotoGridAdapter 类扩展了 ListAdapter,其构造函数需要列表项类型、视图容器以及 DiffUtil.ItemCallback 实现。

在收到请求时,导入 androidx.recyclerview.widget.ListAdaptercom.example.android.marsrealestate.network.MarsProperty 类。在接下来的步骤中,您将实现这个构造函数中会产生错误的其他缺失部分。

class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. 点击 PhotoGridAdapter 类中的任意位置,然后按 Control+i 键以实现 ListAdapter 方法,即 onCreateViewHolder()onBindViewHolder()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented")
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented")
}
  1. PhotoGridAdapter 类定义的末尾,在您刚刚添加的方法之后,为 DiffCallback 添加伴生对象定义,如下所示。

在收到请求时,导入 androidx.recyclerview.widget.DiffUtil

DiffCallback 对象使用您想要比较的对象类型 MarsProperty 来扩展 DiffUtil.ItemCallback

companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Control+i 键,为此对象实现比较条件方法,即 areItemsTheSame()areContentsTheSame()
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented")
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. 对于 areItemsTheSame() 方法,移除 TODO。使用 Kotlin 的指示性等式运算符 (===),它会在 oldItemnewItem 的对象引用相同时返回 true
override fun areItemsTheSame(oldItem: MarsProperty,
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. 对于 areContentsTheSame(),请仅对 oldItemnewItem 的 ID 使用标准结构等式运算符。
override fun areContentsTheSame(oldItem: MarsProperty,
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. PhotoGridAdapter 类中,在伴生对象下方,继续为 MarsPropertyViewHolder 添加内部类定义,该类会扩展 RecyclerView.ViewHolder

如果收到请求,则导入 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding

您需要使用 GridViewItemBinding 变量将 MarsProperty 绑定到布局,以便将变量传递到 MarsPropertyViewHolder。由于基础 ViewHolder 类需要其构造函数中的一个视图,因此您需要向其传递绑定根视图。

class MarsPropertyViewHolder(private var binding:
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {
}
  1. MarsPropertyViewHolder 中,创建一个 bind() 方法,该方法获取 MarsProperty 对象作为参数,并将 binding.property 设置为该对象。设置属性后,请调用 executePendingBindings(),这会导致更新立即执行。
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. onCreateViewHolder()PhotoGridAdapter 类中,继续移除 TODO 并添加如下所示的代码行。在收到请求时,导入 android.view.LayoutInflater

onCreateViewHolder() 方法需要返回新的 MarsPropertyViewHolder,方法是膨胀 GridViewItemBinding 并使用父级 ViewGroup 上下文中的 LayoutInflater

   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. onBindViewHolder() 方法中,移除 TODO 并添加如下所示的代码行。在这一步中,您将调用 getItem() 以获取与当前 RecyclerView 位置关联的 MarsProperty 对象,然后将该资源传递给 MarsPropertyViewHolder 中的 bind() 方法。
val marsProperty = getItem(position)
holder.bind(marsProperty)

第 4 步:添加绑定适配器并连接各部分

最后,使用 BindingAdapter 通过 MarsProperty 对象列表初始化 PhotoGridAdapter。使用 BindingAdapter 设置 RecyclerView 数据会导致数据绑定自动观察 MarsProperty 对象列表的 LiveData。然后,当 MarsProperty 列表发生更改时,系统会自动调用绑定适配器。

  1. 打开 BindingAdapters.kt
  2. 在文件末尾添加 bindRecyclerView() 方法,该方法会获取 RecyclerViewMarsProperty 对象列表作为参数。使用 @BindingAdapter 为该方法添加注解。

在收到请求时,导入 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.network.MarsProperty

@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
    data: List<MarsProperty>?) {
}
  1. bindRecyclerView() 函数中,将 recyclerView.adapter 的类型转换为 PhotoGridAdapter,并用数据调用 adapter.submitList()。此代码会在有新列表可供使用时告知 RecyclerView

如果收到请求,则导入 com.example.android.marsrealestate.overview.PhotoGridAdapter

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. 打开 res/layout/fragment_overview.xml。向 RecyclerView 元素中添加 app:listData 属性,并使用数据绑定将该属性设置为 viewmodel.properties
app:listData="@{viewModel.properties}"
  1. 打开 overview/OverviewFragment.kt。在 onCreateView() 中对 setHasOptionsMenu() 的调用之前,将 binding.photosGrid 中的 RecyclerView 适配器初始化为新的 PhotoGridAdapter 对象。
binding.photosGrid.adapter = PhotoGridAdapter()
  1. 运行应用,您应该会看到一个包含 MarsProperty 图片的网格。在您滚动查看新图片时,该应用会先显示加载进度图标,然后再显示图片本身。如果您开启飞行模式,尚未加载的图片会显示为损坏图片图标。

5d826827fb87002.png

5. 任务:在 RecyclerView 中添加错误处理机制

MarsRealEstate 应用会在无法获取图片时显示损坏图片图标。但在没有网络连接时,应用会显示空白屏幕。

75f8800fef6d5de2.png

这样的用户体验并不是很好。在此任务中,您将添加基本的错误处理机制,以便用户可以清楚地了解所发生的情况。如果无法连接互联网,应用将显示连接错误图标。而在应用提取 MarsProperty 列表时,应用将显示加载动画。

第 1 步:向视图模型添加状态

首先,您将在视图模型中创建一个 LiveData 来表示网络请求的状态。需要考虑以下三种状态:正在加载、成功和失败。在等待对 await() 的调用中的数据时,会出现加载状态。

  1. 打开 overview/OverviewViewModel.kt。在文件的顶部(导入后,在类定义前),添加表示所有可用状态的 enum
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. OverviewViewModel 类中的内部和外部 _response 实时数据定义重命名为 _status。由于您先前在本 Codelab 中添加了对 _properties LiveData 的支持,因此完整的网络服务响应处于尚未使用状态。在这里,您需要使用 LiveData 跟踪当前状态,因此可以只重命名现有变量。

此外,将类型从 String 更改为 MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. 向下滚动到 getMarsRealEstateProperties() 方法,也将这里的 _response 更新为 _status。将 "Success" 字符串更改为 MarsApiStatus.DONE 状态,并将 "Failure" 字符串更改为 MarsApiStatus.ERROR
  2. try {} 代码块前面的状态设置为 MarsApiStatus.LOADING。这是协程运行以及等待数据期间显示的初始状态。完整的 try/catch {} 代码块现在应如下所示:
_status.value = MarsApiStatus.LOADING
try {
   _properties.value = MarsApi.retrofitService.getProperties()
   _status.value = MarsApiStatus.DONE
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. catch {} 代码块中的错误状态后,将 _properties LiveData 设置为空列表。这会清除 RecyclerView
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

第 2 步:为状态 ImageView 添加绑定适配器

现在,视图模型中有一个状态,但它只是一组状态。如何使该状态显示在应用本身中呢?在此步骤中,您将使用已连接到数据绑定的 ImageView 来显示加载和错误状态的图标。当应用处于加载状态或错误状态时,ImageView 应可见。应用完成加载后,ImageView 应不可见。

  1. 打开 BindingAdapters.kt。添加一个名为 bindStatus() 的新绑定适配器,该适配器获取 ImageViewMarsApiStatus 值作为参数。在收到请求时,导入 com.example.android.marsrealestate.overview.MarsApiStatus
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
          status: MarsApiStatus?) {
}
  1. bindStatus() 方法中添加一个 when {} 代码块,以在不同状态之间切换。
when (status) {

}
  1. when {} 中,为加载状态 (MarsApiStatus.LOADING) 添加一个用例。对于此状态,请将 ImageView 设为可见,然后为其分配加载动画。这与您在上一个任务中用于 Glide 的动画可绘制对象相同。在收到请求时,导入 android.view.View
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. 为错误状态 MarsApiStatus.ERROR 添加一个用例。与针对 LOADING 状态的操作类似,将状态 ImageView 设置为可见,并重新使用连接错误可绘制对象。
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 为完成状态 MarsApiStatus.DONE 添加一个用例。在这一步中,您获得了成功响应,因此请关闭状态 ImageView 的可见性,将其隐藏。
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

第 3 步:向布局添加状态 ImageView

  1. 打开 res/layout/fragment_overview.xml。在 ConstraintLayout 内的 RecyclerView 元素下,添加如下所示的 ImageView

ImageViewRecyclerView 具有相同的限制条件。不过,宽度和高度使用 wrap_content 使图片居中,而不是拉伸图片填满视图。另请注意 app:marsApiStatus 属性,该属性会在视图模型中的状态属性发生更改时让视图调用 BindingAdapter

<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}" />
  1. 在模拟器或设备中开启飞行模式即可模拟网络连接缺失的情况。编译并运行应用,并注意显示的错误图片:

eb0d20401060679c.png

  1. 点按“返回”按钮以关闭应用,然后关闭飞行模式。使用“最近”屏幕返回应用。根据网络连接速度,如果应用在开始加载图片之前查询网络服务,您可能会看到一个非常简单的旋转图标。

6. 解决方案代码

Android Studio 项目:MarsRealEstateGrid

7. 总结

  • 如需简化图片管理流程,请使用 Glide 库在应用中下载、缓冲、解码以及缓存图片。
  • Glide 需要具备以下两项内容才能从互联网加载图片:图片的网址,以及用于容纳图片的 ImageView 对象。如需指定这些选项,请将 load()into() 方法与 Glide 搭配使用。
  • 绑定适配器是位于视图与视图的绑定数据之间的扩展方法。绑定适配器可以在数据发生更改时提供自定义行为,例如,调用 Glide 将来自网址的图片加载到 ImageView 中。
  • 绑定适配器是带有 @BindingAdapter 注解的扩展方法。
  • 如需向 Glide 请求添加选项,请使用 apply() 方法。例如,将 apply()placeholder() 搭配使用可指定加载可绘制对象,将 apply()error() 搭配使用可指定错误可绘制对象。
  • 如需生成图片网格,请将 RecyclerViewGridLayoutManager 结合使用。
  • 如需在属性发生更改时更新属性列表,请在 RecyclerView 和布局之间使用绑定适配器。

8. 了解详情

Udacity 课程:

Android 开发者文档:

其他:

9. 家庭作业

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

哪个 Glide 方法用于指明将包含已加载图片的 ImageView

into()

with()

imageview()

apply()

问题 2

如何指定在加载 Glide 时显示的占位符图片?

▢ 使用包含可绘制对象的 into() 方法。

▢ 使用 RequestOptions() 并调用包含可绘制对象的 placeholder() 方法。

▢ 将 Glide.placeholder 属性分配给可绘制对象。

▢ 使用 RequestOptions() 并调用包含可绘制对象的 loadingImage() 方法。

问题 3

如何指明某个方法是否为绑定适配器?

▢ 对 LiveData 调用 setBindingAdapter() 方法。

▢ 将该方法放在名为 BindingAdapters.kt 的 Kotlin 文件中。

▢ 在 XML 布局中使用 android:adapter 属性。

▢ 为该方法添加 @BindingAdapter 注解。

10. 下一个 Codelab

开始学习下一课:

如需本课程中其他 Codelab 的链接,请查看“Android Kotlin 基础知识”Codelab 着陆页