載入及顯示網際網路上的圖片

1. 歡迎

說明

在之前的程式碼研究室中,您已瞭解如何從網路服務獲取資料,並將回應剖析為 Kotlin 物件。在本程式碼研究室中,您可以在該知識的基礎上,瞭解如何從網址載入並顯示相片。您也可以回顧如何建構 RecyclerView 並使用它在總覽頁面上顯示圖片格線。

必要條件

  • 如何建立及使用片段。
  • 如何從 REST 網路服務擷取 JSON,並使用 RetrofitMoshi 程式庫將資料剖析為 Kotlin 物件。
  • 如何使用 RecyclerView 建構格線版面配置。
  • AdapterViewHolderDiffUtil 的運作方式。

課程內容

  • 如何使用 Coil 程式庫從網址載入及顯示圖片。
  • 如何使用 RecyclerView 和格線轉接器顯示圖片格線。
  • 如何處理圖片下載及顯示時出現的潛在錯誤。

建構項目

  • 修改 MarsPhotos 應用程式以取得 Mars 資料中的圖片網址,並使用 Coil 載入並顯示該圖片。
  • 在應用程式中加入載入動畫和錯誤圖示。
  • 使用 RecyclerView 顯示 Mars 圖片格線。
  • RecyclerView 加入狀態和錯誤處理機制。

需求條件

  • 安裝了新式網路瀏覽器的電腦,例如最新版 Chrome
  • 可以連上網際網路的電腦。

2. 應用程式總覽

在本程式碼研究室中,您將繼續使用之前程式碼研究室中稱為 MarsPhotos 的應用程式。MarsPhotos 應用程式會連線至網路服務,以擷取並顯示使用 Retrofit 獲取的 Kotlin 物件數目。這些 Kotlin 物件包含 NASA 的火星探測器擷取的火星表面真實相片的網址。

您在本程式碼研究室中建構的應用程式版本會填入總覽頁面,此頁面以圖片格線模式顯示火星相片。這些圖片來自您的應用程式從 Mars 網路服務擷取的資料。您的應用程式會使用 Coil 程式庫來載入並顯示圖片,也會使用 RecyclerView 來建立圖片的格線版面配置。此外,應用程式還會妥善處理網路錯誤。

243d21747dfb8999.png

3. 顯示網際網路圖片

想要顯示某個網址的相片,聽起來可能很簡單,但其中需要經過很多程序才能順利完成。圖片必須經過下載、內部儲存、對壓縮格式進行解碼,才能供 Android 使用。圖片應快取至記憶體快取或儲存空間快取,或同時存放在這兩個地方。系統只會在低優先順序的背景執行緒中採取這些動作,以確保使用者介面的靈敏度。此外,為獲得最佳網路和 CPU 效能,建議您一次擷取多張圖片並解碼。

您可以使用社群開發的資料庫 Coil 下載、緩衝處理、解碼及快取圖片。如果不使用 Coil,工作將會更多。

Coil 基本上需要下列兩項:

  • 要載入並顯示的圖片網址。
  • 用來顯示該圖片的 ImageView 物件。

在這項工作中,您會瞭解如何使用 Coil 顯示來自 Mars 網路服務的單張圖片。您會顯示網路服務傳回的相片清單中的第一張火星相片。以下是前後對照的螢幕截圖:

新增 Coil 依附元件

  1. 開啟先前程式碼研究室中的 MarsPhotos 解決方案應用程式。
  2. 執行應用程式以查看其用途。(其中顯示擷取的火星相片總數)。
  3. 開啟「build.gradle (Module: app)」
  4. dependencies 區段,為 Coil 程式庫新增此行內容:
    // Coil
    implementation "io.coil-kt:coil:1.1.1"

透過 Coil 說明文件頁面查看及更新最新版本的程式庫。

  1. Coil 程式庫於 mavenCentral() 存放區託管並提供。在 build.gradle (Project: MarsPhotos) 的頂部 repositories 區塊中,新增 mavenCentral()
repositories {
   google()
   mavenCentral()
}
  1. 按一下「Sync Now」,使用新依附元件重新建構專案。

更新 ViewModel

在這個步驟中,您需要將 LiveData 屬性新增至 OverviewViewModel 類別,以儲存收到的 Kotlin 物件 MarsPhoto。

  1. 開啟 overview/OverviewViewModel.kt。在 _status 屬性宣告的正下方,新增類型為 MutableLiveData 的可變動屬性 _photos,其可儲存單個 MarsPhoto 物件。
private val _photos = MutableLiveData<MarsPhoto>()

依要求匯入 com.example.android.marsphotos.network.MarsPhoto

  1. _photos 宣告的正下方,新增類型為 LiveData<MarsPhoto> 的公開支援欄位 photos
val photos: LiveData<MarsPhoto> = _photos
  1. getMarsPhotos() 方法的 try{} 區塊中,找到以下一行內容,可用於將從網路服務擷取的資料設為 listResult.
try {
   val listResult = MarsApi.retrofitService.getPhotos()
   ...
}
  1. 將擷取到的第一張火星相片指派給新的變數 _photos。將 listResult 變更為 _photos.value。在索引 0 處指派第一個相片網址。這將引發錯誤,您要在稍後修正它。
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   ...
}
  1. 在下一行中,將 status.value 更新為下列內容。請使用新屬性 (非 listResult 中) 的資料。顯示相片清單中的第一個圖片網址。
try {
   ...
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"

}
  1. 現在,完整的 try{} 區塊大致如下:
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
  1. 執行應用程式。現在,TextView 會顯示第一張火星相片的網址。目前,您已設定好該網址的 ViewModelLiveData

ae99ec8569198456.png

使用繫結轉接器

繫結轉接器是帶註解的方法,用於為檢視畫面的自訂屬性建立自訂 setter。

通常您會使用以下程式碼在 XML 中設定屬性:android:text="Sample Text"。Android 系統會自動尋找由 setText(String: text) 方法設定、名稱與 text 屬性相同的 setter 屬性。setText(String: text) 方法是一種 setter 方法,適用於 Android 架構所提供之部分檢視畫面。您可使用繫結轉接器自訂類似行為;也可以提供由資料繫結程式庫呼叫的自訂屬性和自訂邏輯。

範例:

若想執行更為複雜的操作,而不只是在圖片檢視畫面上呼叫 setter,請設定可繪圖片。請考慮從網際網路載入 UI 執行緒 (主要執行緒) 的圖片。首先,選擇自訂屬性用於將圖片指派給 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 程式庫會載入 UI 執行緒的圖片並將其設為 ImageView

建立繫結轉接器及使用 Coil

  1. com.example.android.marsphotos 套件中,建立名稱為 BindingAdapters 的 Kotlin 檔案。這個檔案會保留您在應用程式中使用的繫結轉接器。

a04afbd6ae8ccfcd.png

  1. BindingAdapters.kt 中,建立 bindImage() 函式做為頂層函式 (不在類別中),並使用 ImageViewString 做為參數。
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

依要求匯入 android.widget.ImageView

  1. 使用 @BindingAdapter 為函式加上註解。@BindingAdapter 註解會通知資料繫結,在檢視畫面項目擁有 imageUrl 屬性時執行此繫結轉接器。
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

依要求匯入 androidx.databinding.BindingAdapter

let 範圍函式

let 是 Kotlin 的範圍函式之一,可讓您在物件環境內執行程式碼區塊。Kotlin 中有五個範圍函式,詳情請參閱說明文件

使用方式:

let 的用途是根據呼叫鏈的結果叫用一或多個函式。

let 函式和安全呼叫運算子 (?.) 的用途是對物件執行空值安全運算。在這種情況下,只有在物件不是空值時,系統才會執行 let 程式碼區塊。

  1. bindImage() 函式中,使用安全呼叫運算子將 let{} 區塊新增至 imgUrl 引數。
imgUrl?.let {
}
  1. let{} 區塊內,新增以下一行內容來使用 toUri() 方法將網址字串轉換為 Uri 物件。若要使用 HTTPS 配置,請將 buildUpon.scheme("https") 附加至 toUri 建構工具。接著呼叫 build() 來建構物件。
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

依要求匯入 androidx.core.net.toUri

  1. let{} 區塊內,宣告 imgUri 之後,使用 Coilload(){}imgUri 物件的圖片載入 imgView
imgView.load(imgUri) {
}

依要求匯入 coil.load

  1. 完整方法大致如下:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
        imgView.load(imgUri)
    }
}

更新版面配置和片段

在上一節中,您使用了 Coil 圖片庫載入圖片。接著使用新屬性更新 ImageView,以顯示單張圖片,即可在螢幕上顯示圖片。

稍後,在程式碼研究室中,您將使用 res/layout/grid_view_item.xml 做為 RecyclerView 中每個格線項目的版面配置資源檔案。在這項工作中,您將暫時使用這個檔案,透過在前一項工作中擷取到的圖片網址來顯示圖片。目前,您將使用這個版面配置檔案取代 fragment_overview.xml

  1. 開啟 res/layout/grid_view_item.xml
  2. <ImageView> 元素上方,為資料繫結新增 <data> 元素,並繫結至 OverviewViewModel 類別:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
  1. ImageView 元素中新增 app:imageUrl 屬性,即可使用新的圖片載入繫結轉接器。請注意,photos 包含從伺服器擷取的清單 MarsPhotos。將第一個項目網址指派給 imageUrl 屬性。
    <ImageView
        android:id="@+id/mars_image"
        ...
        app:imageUrl="@{viewModel.photos.imgSrcUrl}"
        ... />
  1. 開啟 overview/OverviewFragment.kt。在 onCreateView() 方法中,對於加載 FragmentOverviewBinding 類別並將其指派給繫結變數的行,排除其註解。移除此行會發生錯誤。請放心,這是暫時現象,稍後就會修正。
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. 請改用 grid_view_item.xml 取代 fragment_overview.xml.。在這種情況下,請改為新增以下一行內容,以加載 GridViewItemBinding 類別。
val binding = GridViewItemBinding.inflate(inflater)

視需要匯入 com.example.android.marsphotos. databinding.GridViewItemBinding

  1. 執行應用程式。現在螢幕上會顯示單張火星圖片。

e59b6e849e63ae2b.png

新增載入和錯誤圖片

使用 Coil 不僅可以顯示預留位置圖片,還能載入圖片,如果因圖片缺失或損毀等原因造成載入失敗,也可以載入錯誤圖片,進而改善使用者體驗。在這個步驟中,您需要為繫結轉接器新增該功能。

  1. 開啟 res/drawable/ic_broken_image.xml,然後按一下右側的「Design」分頁標籤。如果出現錯誤圖片,表示您使用的是內建圖示庫中的無效圖片圖示。此向量可繪項目使用 android:tint 屬性,將圖示變為灰色。

467c213c859e1904.png

  1. 開啟 res/drawable/loading_animation.xml。這一可繪項目是個動畫,可圍繞聚焦點旋轉圖片可繪項目 loading_img.xml (預覽畫面不會顯示動畫)。

6c1f87d1c932c762.png

  1. 返回 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)
}
  1. 現在,完整的 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)
        }
    }
}
  1. 執行應用程式。系統可能會在 Coil 下載並顯示屬性圖片時,暫時顯示載入圖片,具體情況視網路連線速度而定。但即使您關閉網路,此刻仍不會顯示毀損的圖片圖示。此問題會在程式碼研究室的最後一項工作中加以修正。

6dcecd205a0741a.gif

  1. 還原您在 overview/OverviewFragment.kt 中做出的臨時變更。在方法 onCreateview() 中,取消註解加載 FragmentOverviewBinding 的行。刪除加載 GridViewIteMBinding 的行或排除其註解。
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)

4. 透過 RecyclerView 顯示圖片格線

現在,您的應用程式會從網際網路載入火星相片。透過使用第一個 MarsPhoto 清單項目的資料,您已在 ViewModel 中建立 LiveData 屬性,並使用 Mars 相片資料中的圖片網址填入 ImageView。但我們的目標是讓您的應用程式顯示圖片格線,因此在這項工作中,您將使用 RecyclerView 搭配格線版面配置管理工具來顯示圖片格線。

更新檢視畫面模型

在先前的工作中,您在 OverviewViewModel 新增了名為 _photosLiveData 物件,該物件存放了一個 MarsPhoto 物件,即網路服務提供的回應清單中的第一個物件。在此步驟中,您必須變更這個 LiveData,使其存放 MarsPhoto 物件的完整清單。

  1. 開啟 overview/OverviewViewModel.kt
  2. _photos 類型變更為 MarsPhoto 物件的清單。
private val _photos = MutableLiveData<List<MarsPhoto>>()
  1. 同時將備用屬性 photos 類型變更為 List<MarsPhoto> 類型:
 val photos: LiveData<List<MarsPhoto>> = _photos
  1. 向下捲動至 getMarsPhotos() 方法中的 try {} 區塊。MarsApi.retrofitService.getPhotos()

會傳回 MarsPhoto 物件的清單,您可以將它指派給 _photos.value

_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
  1. 現在整個 try/catch 區塊如下所示:
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
    _status.value = "Failure: ${e.message}"
}

格線版面配置

RecyclerViewGridLayoutManager 會將資料版面配置為可捲動的格線,如下所示。

fcf0fc4b78f8650.png

從設計的角度來看,格線版面配置最適合用於可透過圖示或圖片表示的清單,例如火星相片瀏覽應用程式中的清單。

格線版面配置的項目排列方式

格線版面配置透過由列和欄組成的格線排列項目。假設您預設為垂直捲動,則每一列中的每個項目都會占用一個「跨距」。一個項目可以占用多個跨距。在下列範例中,一個跨距等同於一欄的寬度 (3)。

在以下兩個範例中,每列由三個跨距組成。根據預設,GridLayoutManager 會將每個項目放置在一個跨距中,直到達到您指定的跨距計數。達到跨距計數時,系統會換行到下一行。

新增 Recyclerview

在這個步驟中,您需要變更應用程式的版面配置,改用採用格線版面配置的回收器檢視畫面,而非單張圖片檢視畫面。

  1. 開啟 layout/grid_view_item.xml。請移除 viewModel 資料變數。
  2. <data> 標記中,新增下列 MarsPhoto 類型的 photo 變數。
<data>
   <variable
       name="photo"
       type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
  1. <ImageView> 中,將 app:imageUrl 屬性變更為參照 MarsPhoto 物件中的圖片網址。這些變更會復原您在上一個工作中做出的臨時變更。
app:imageUrl="@{photo.imgSrcUrl}"
  1. 開啟 layout/fragment_overview.xml。刪除整個 <TextView> 元素。
  2. 請改為新增下列 <RecyclerView> 元素。將 ID 設為 photos_grid,將 widthheight 屬性設為 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" />
  1. 如要預覽上述程式碼在「Design」檢視畫面中的外觀,請使用 tools:itemCount 將版面配置中顯示的項目數目設為 16itemCount 屬性可指定版面配置編輯器在「Preview」視窗中應顯示的項目數量。使用 tools:listitem 將清單項目的版面配置設為 grid_view_item
<androidx.recyclerview.widget.RecyclerView
            ...
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />
  1. 切換為「Design」檢視畫面後,系統應會顯示類似下方螢幕截圖的預覽畫面。這個預覽畫面看起來不像火星相片,但您可以從中查看 recyclerview 格線版面配置的外觀。對於 recyclerview 中每個格線項目,其預覽皆會顯示邊框間距和 grid_view_item 版面配置。

20742824367c3952.png

  1. 根據 Material Design 規範,清單頂端、底部和側邊應保留 8dp 的空間,而項目之間應保留 4dp 的空間。搭配運用 fragment_overview.xml 版面配置和 grid_view_item.xml 版面配置中的邊框間距可實現上述目標。

a3561fa85fea7a8f.png

  1. 開啟 layout/gridview_item.xml。請注意,padding 屬性在項目外部和內容之間留有 2dp 的邊框間距。如此可在項目內容之間留出 4dp 的空間,而在外邊留出 2dp 的空間。也就是說,為了符合設計規範,外邊需要留出額外 6dp 的邊框間距。
  2. 返回 layout/fragment_overview.xml。為 RecyclerView 新增 6dp 的邊框間距,這樣外部就有 8dp,內部則有 4dp,符合規範。
<androidx.recyclerview.widget.RecyclerView
            ...
            android:padding="6dp"
            ...  />
  1. 完整的 <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"  />

新增相片格線轉接器

現在,RecyclerViewfragment_overview 版面配置中採用格線版面配置。在這個步驟中,您可以透過 RecyclerView 轉接器,將從網路伺服器擷取的資料繫結至 RecyclerView

ListAdapter (重新整理程式)

ListAdapterRecyclerView.Adapter 類別的子類別,用來在 RecyclerView 中顯示清單資料,包括在背景執行緒上計算清單之間的差異。

在此應用程式中,您將在 ListAdapter. 中使用 DiffUtil 實作。使用 DiffUtil 的優勢在於,每當新增、移除或變更 RecyclerView 中的某些項目時,系統不會重新整理整個清單。系統只會重新整理已變更的項目。

ListAdapter 新增至您的應用程式。

  1. overview 套件中,建立名為 PhotoGridAdapter 的 Kotlin 類別。
  2. 使用如下所示的建構函式參數,從 ListAdapter 擴充 PhotoGridAdapter 類別。PhotoGridAdapter 類別會擴充 ListAdapter,其建構函式需要清單項目類型、檢視畫面持有者和 DiffUtil.ItemCallback 實作。
class PhotoGridAdapter : ListAdapter<MarsPhoto,
        PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}

視需要匯入 androidx.recyclerview.widget.ListAdaptercom.example.android.marsphoto.network.MarsPhoto 類別。在後續步驟中,您將導入此建構函式缺少的其他實作,這些實作會產生錯誤。

  1. 如要解決上述錯誤,請在這個步驟中新增必要的方法,並於這項工作的後續部分實作這些方法。依序按一下 PhotoGridAdapter 類別、紅色燈泡,然後從下拉式選單中選取「Implement members」。在彈出式視窗中,選取 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")
}

實作 onCreateViewHolderonBindViewHolder 方法時需要 MarsPhotoViewHolder,這個項目會在下一步中新增。

  1. PhotoGridAdapter 中,新增 MarsPhotoViewHolder 的內部類別定義,該定義可擴充 RecyclerView.ViewHolder。您需要使用 GridViewItemBinding 變數將 MarsPhoto 繫結至版面配置,請將該變數傳送至 MarsPhotoViewHolder。基本 ViewHolder 類別需要在其建構函式中設定檢視畫面,而您要將其傳送至繫結的根檢視畫面。
class MarsPhotoViewHolder(private var binding:
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {
}

視需要匯入 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding

  1. MarsPhotoViewHolder 中建立 bind() 方法,將 MarsPhoto 物件做為引數,並將 binding.property 設為該物件。設定屬性後,請呼叫 executePendingBindings(),以立即執行更新作業。
fun bind(MarsPhoto: MarsPhoto) {
   binding.photo = MarsPhoto
   binding.executePendingBindings()
}
  1. 還是在 onCreateViewHolder()PhotoGridAdapter 類別中,移除 TODO 並新增下列一行內容。onCreateViewHolder() 方法需要傳回新的 MarsPhotoViewHolder,加載 GridViewItemBinding 並使用父項 ViewGroup 背景資訊中的 LayoutInflater 即可建立。
   return MarsPhotoViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))

視需要匯入 android.view.LayoutInflater

  1. onBindViewHolder() 方法中,移除 TODO 並新增下列幾行內容。您可以在這裡呼叫 getItem(),取得與目前 RecyclerView 位置相關的 MarsPhoto 物件,然後將該屬性傳遞至 MarsPhotoViewHolder 中的 bind() 方法。
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
  1. PhotoGridAdapter 中,為 DiffCallback 新增隨附物件定義,如下所示。
    DiffCallback 物件會將 DiffUtil.ItemCallback 擴充為要比較的一般物件類型:MarsPhoto。您需要在這項實作中比較兩張火星相片物件。
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}

依要求匯入 androidx.recyclerview.widget.DiffUtil

  1. 按下紅色燈泡,為 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") }
  1. areItemsTheSame() 方法中,移除 TODODiffUtil 會呼叫此方法來判定兩個物件是否代表同一個項目。DiffUtil 會使用此方法來判斷新的 MarsPhoto 物件是否和舊的 MarsPhoto 物件相同。每個項目 (MarsPhoto 物件) 的 ID 皆不得重複。比較 oldItemnewItem 的 ID,然後傳回結果。
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.id == newItem.id
}
  1. areContentsTheSame() 中,移除 TODO。如果 DiffUtil 想檢查兩個項目是否擁有相同的資料,便會呼叫此方法。MarsPhoto 中的重要資料是圖片網址。比較 oldItemnewItem 的網址,然後傳回結果。
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.imgSrcUrl == newItem.imgSrcUrl
}

請確認您可以正常編譯並執行應用程式,但模擬器顯示空白畫面。recyclerview 已準備就緒,但尚未收到任何資料,您將在接下來的步驟中實作這部分內容。

新增繫結轉接器並連結各個部分

在這個步驟中,您將使用 BindingAdapter 來初始化包含 MarsPhoto 物件清單的 PhotoGridAdapter。若使用 BindingAdapter 設定 RecyclerView 資料,資料繫結將自動監控 LiveDataMarsPhoto 物件清單。當 MarsPhoto 清單有所變更時,系統會自動呼叫繫結轉接器。

  1. 開啟 BindingAdapters.kt
  2. 在檔案結尾,新增 bindRecyclerView() 方法,以 RecyclerViewMarsPhoto 物件清單做為引數。請使用帶有 listData 屬性的 @BindingAdapter 為該方法加上註解。
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
    data: List<MarsPhoto>?) {
}

視需要匯入 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsphotos.network.MarsPhoto

  1. bindRecyclerView() 函式中,將 recyclerView.adapter 投放至 PhotoGridAdapter 並指派給新的 val 屬性 adapter.
val adapter = recyclerView.adapter as PhotoGridAdapter
  1. bindRecyclerView() 函式結尾,呼叫 adapter.submitList() 可查看火星相片清單資料。出現新的清單時,這個屬性會通知 RecyclerView
adapter.submitList(data)

視需要匯入 com.example.android.marsrealestate.overview.PhotoGridAdapter

  1. 完整的 bindRecyclerView 繫結轉接器應如下所示:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
                    data: List<MarsPhoto>?) {
   val adapter = recyclerView.adapter as PhotoGridAdapter
   adapter.submitList(data)

}
  1. 如要連結所有項目,請開啟 res/layout/fragment_overview.xml。接著將 app:listData 屬性新增至 RecyclerView 元素,然後使用資料繫結將屬性設為 viewmodel.photos。這類似於您在先前的工作中為 ImageView 完成的工作。
app:listData="@{viewModel.photos}"
  1. 開啟 overview/OverviewFragment.kt。在 onCreateView() 中,於 return 陳述式之前,將 binding.photosGrid 中的 RecyclerView 轉接器初始化為新的 PhotoGridAdapter 物件。
binding.photosGrid.adapter = PhotoGridAdapter()
  1. 執行應用程式。畫面上應該會顯示捲動的火星圖片格線。捲動畫面即可查看新圖片,但看起來有點奇怪。當您捲動畫面時,邊框間距會維持在 RecyclerView 的頂端和底部,所以清單看起來並不會在動作列下方捲動。

5d03641aa1589842.png

  1. 如要修正這個問題,您需要指示 RecyclerView 不要使用 android:clipToPadding 屬性將內部內容剪輯至邊框間距。這樣,它便會在邊框間距區域中繪製捲動檢視畫面。返回 layout/fragment_overview.xml。為 RecyclerView 新增 android:clipToPadding 屬性,並將其設為 false
<androidx.recyclerview.widget.RecyclerView
            ...
            android:clipToPadding="false"
            ...  />
  1. 執行應用程式。請注意,應用程式在正常顯示圖片前還會顯示載入進度圖示。這是您傳送給 Coil 圖片庫的預留位置載入圖片。

3128b84aa22ef97e.png

  1. 執行應用程式期間,請開啟飛航模式。在模擬器中捲動圖片。尚未載入的圖片會顯示為無效圖片圖示。這是您傳遞至 Coil 圖片庫的圖片可繪項目,用於在系統出現網路錯誤或無法擷取圖片時顯示。

28d2cbba564f35ff.png

恭喜您,就快完成了!在接下來的最後一項工作中,您需要在應用程式中新增更多錯誤處理機制,進一步改善使用者體驗。

5. 在 RecyclerView 中新增錯誤處理機制

MarsPhotos 應用程式會將無法擷取的圖片顯示為無效圖片圖示。但是如果沒有網路連線,應用程式會顯示空白畫面。您將在接下來的步驟中驗證空白畫面。

  1. 開啟裝置或模擬器上的飛航模式。透過 Android Studio 執行應用程式。請注意空白畫面。

492011786c2dd7f7.png

這無法提供良好的使用者體驗。在這項工作中,您需要新增基本的錯誤處理機制,讓使用者更好地瞭解發生的情況。如果未連線到網際網路,應用程式將顯示連線錯誤圖示;而在擷取 MarsPhoto 清單時,應用程式會顯示載入動畫。

在 ViewModel 中新增狀態

在這項工作中,您會在 OverviewViewModel 中建立屬性來代表網路要求的狀態。需要考慮的狀態有三種:載入、成功和故障。當您等待資料時,畫面會顯示載入狀態。系統成功從網路服務擷取資料時會顯示成功狀態。出現網路或連線錯誤時會顯示故障狀態。

Kotlin 中的列舉類別

如要在應用程式中表示這三個狀態,請使用 enumenum 是列舉的縮寫,表示集合中所有項目已排序的清單。每個 enum 常數都是 enum 類別的一個物件。

在 Kotlin 中,enum 這種資料類型可容納一組常數。其定義方式是在類別定義前方加上關鍵字 enum,如下所示。列舉常數會以半形逗號分隔。

定義:

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

使用方式:

var direction : Direction = Direction.NORTH

如上所示,您可以使用類別名稱後跟點 (.) 運算子和常數名稱來參照 enum 物件。

在 Viewmodel 中新增帶有狀態值的列舉類別定義。

  1. 開啟 overview/OverviewViewModel.kt。然後在檔案頂端 (匯入之後、類別定義之前) 新增 enum,代表所有可用狀態:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. 捲動至 _statusstatus 屬性的定義後,將類型從 String 變更為 MarsApiStatus. MarsApiStatus。是您在上一步定義的列舉類別。
private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus> = _status
  1. getMarsPhotos() 方法中,將 "Success: ..." 字串變更為 MarsApiStatus.DONE 狀態,並將 "Failure..." 字串變更為 MarsApiStatus.ERROR
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = MarsApiStatus.DONE
} catch (e: Exception) {
     _status.value = MarsApiStatus.ERROR
}
  1. 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
            }
        }
  1. catch {} 區塊中的錯誤狀態之後,將 _photos 設為空白清單。這項操作可清除回收器檢視畫面。
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _photos.value = listOf()
}
  1. 完整的 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

  1. 請開啟 BindingAdapters.kt,捲動至檔案結尾,即可新增其他轉接器。新增名為 bindStatus() 的新繫結轉接器,以 ImageViewMarsApiStatus 值做為引數。使用 @BindingAdapter 為方法加上注釋,傳遞自訂屬性 marsApiStatus 做為參數。
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
          status: MarsApiStatus?) {
}

視需要匯入 com.example.android.marsrealestate.overview.MarsApiStatus

  1. bindStatus() 方法中新增 when {} 區塊,即可在不同狀態之間切換。
when (status) {

}
  1. when {} 中,新增載入狀態的案例 (MarsApiStatus.LOADING)。對於這個狀態,請將 ImageView 設為顯示,並指派載入動畫。這個動畫可繪項目與先前工作中 Coil 採用的可繪項目相同。
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}

視需要匯入 android.view.View

  1. 新增錯誤狀態的案例,即 MarsApiStatus.ERROR。如同 LOADING 狀態,請將狀態 ImageView 設為顯示,並使用連線錯誤可繪項目。
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 新增已完成狀態的案例,即 MarsApiStatus.DONE。您收到了正確的回應,請將狀態 ImageView 的瀏覽權限設定為 View.GONE 以隱藏該狀態。
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

您已為狀態圖片檢視畫面設定了繫結轉接器,在下一個步驟中,您需要新增圖片檢視畫面,該檢視畫面使用新的繫結轉接器。

新增狀態 ImageView

在這個步驟中,您將在 fragment_overview.xml 中新增圖片檢視畫面,以顯示您先前定義的狀態。

  1. 開啟 res/layout/fragment_overview.xml。在 ConstraintLayoutRecyclerView 元素下方,新增下方顯示的 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}" />

上述 ImageViewRecyclerView 具有相同的約束。不過,所設寬度和高度會使用 wrap_content 將圖片置中,而不是延展圖片來填滿檢視畫面。另請注意,app:marsApiStatus 屬性已設為 viewModel.status,並在 ViewModel 中的狀態屬性變更時呼叫 BindingAdapter

  1. 如要測試上述程式碼,請在模擬器或裝置上開啟飛航模式,模擬網路連線錯誤。接著編譯並執行應用程式,請注意,畫面上會顯示錯誤圖片:

a91ddb1c89f2efec.png

  1. 輕觸返回按鈕即可關閉應用程式,並關閉飛航模式。使用最近畫面可返回應用程式。當應用程式查詢網路服務時,圖片開始載入之前,畫面上可能會極短暫地顯示載入旋轉圖示,具體視您的網路連線速度而定。

恭喜您完成本程式碼研究室,並建構了 MarsPhotos 應用程式!現在就與親朋好友分享真實的火星圖片,炫耀一下您的應用程式吧。

6. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於下方顯示的專案中。使用 main 分支版本提取或下載程式碼。

如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

7. 匯總

  • Coil 程式庫可簡化在應用程式中管理圖片的程序,例如下載、緩衝處理、解碼和快取圖片。
  • 繫結轉接器是一種擴充方法,顯示在檢視畫面與檢視畫面的繫結資料之間。資料變更時,繫結轉接器會提供自訂行為,例如呼叫 Coil 將網址中的圖片載入 ImageView
  • 繫結轉接器是擴充方法,附有 @BindingAdapter 註解。
  • 如要顯示圖片格線,請將 RecyclerViewGridLayoutManager 搭配使用。
  • 如要在屬性變更時更新屬性清單,請使用 RecyclerView 和版面配置之間的繫結轉接器。

8. 瞭解詳情

Android 開發人員說明文件:

其他: