1. 歡迎
說明
在之前的程式碼研究室中,您已瞭解如何從網路服務獲取資料,並將回應剖析為 Kotlin 物件。在本程式碼研究室中,您可以在該知識的基礎上,瞭解如何從網址載入並顯示相片。您也可以回顧如何建構 RecyclerView
並使用它在總覽頁面上顯示圖片格線。
必要條件
- 如何建立及使用片段。
- 如何從 REST 網路服務擷取 JSON,並使用 Retrofit 和 Moshi 程式庫將資料剖析為 Kotlin 物件。
- 如何使用
RecyclerView
建構格線版面配置。 Adapter
、ViewHolder
和DiffUtil
的運作方式。
課程內容
- 如何使用 Coil 程式庫從網址載入及顯示圖片。
- 如何使用
RecyclerView
和格線轉接器顯示圖片格線。 - 如何處理圖片下載及顯示時出現的潛在錯誤。
建構項目
- 修改 MarsPhotos 應用程式以取得 Mars 資料中的圖片網址,並使用 Coil 載入並顯示該圖片。
- 在應用程式中加入載入動畫和錯誤圖示。
- 使用
RecyclerView
顯示 Mars 圖片格線。 - 為
RecyclerView
加入狀態和錯誤處理機制。
需求條件
- 安裝了新式網路瀏覽器的電腦,例如最新版 Chrome。
- 可以連上網際網路的電腦。
2. 應用程式總覽
在本程式碼研究室中,您將繼續使用之前程式碼研究室中稱為 MarsPhotos 的應用程式。MarsPhotos 應用程式會連線至網路服務,以擷取並顯示使用 Retrofit 獲取的 Kotlin 物件數目。這些 Kotlin 物件包含 NASA 的火星探測器擷取的火星表面真實相片的網址。
您在本程式碼研究室中建構的應用程式版本會填入總覽頁面,此頁面以圖片格線模式顯示火星相片。這些圖片來自您的應用程式從 Mars 網路服務擷取的資料。您的應用程式會使用 Coil 程式庫來載入並顯示圖片,也會使用 RecyclerView
來建立圖片的格線版面配置。此外,應用程式還會妥善處理網路錯誤。
3. 顯示網際網路圖片
想要顯示某個網址的相片,聽起來可能很簡單,但其中需要經過很多程序才能順利完成。圖片必須經過下載、內部儲存、對壓縮格式進行解碼,才能供 Android 使用。圖片應快取至記憶體快取或儲存空間快取,或同時存放在這兩個地方。系統只會在低優先順序的背景執行緒中採取這些動作,以確保使用者介面的靈敏度。此外,為獲得最佳網路和 CPU 效能,建議您一次擷取多張圖片並解碼。
您可以使用社群開發的資料庫 Coil 下載、緩衝處理、解碼及快取圖片。如果不使用 Coil,工作將會更多。
Coil 基本上需要下列兩項:
- 要載入並顯示的圖片網址。
- 用來顯示該圖片的
ImageView
物件。
在這項工作中,您會瞭解如何使用 Coil 顯示來自 Mars 網路服務的單張圖片。您會顯示網路服務傳回的相片清單中的第一張火星相片。以下是前後對照的螢幕截圖:
新增 Coil 依附元件
- 開啟先前程式碼研究室中的 MarsPhotos 解決方案應用程式。
- 執行應用程式以查看其用途。(其中顯示擷取的火星相片總數)。
- 開啟「build.gradle (Module: app)」。
- 在
dependencies
區段,為 Coil 程式庫新增此行內容:
// Coil
implementation "io.coil-kt:coil:1.1.1"
透過 Coil 說明文件頁面查看及更新最新版本的程式庫。
- Coil 程式庫於
mavenCentral()
存放區託管並提供。在 build.gradle (Project: MarsPhotos) 的頂部repositories
區塊中,新增mavenCentral()
。
repositories {
google()
mavenCentral()
}
- 按一下「Sync Now」,使用新依附元件重新建構專案。
更新 ViewModel
在這個步驟中,您需要將 LiveData
屬性新增至 OverviewViewModel
類別,以儲存收到的 Kotlin 物件 MarsPhoto。
- 開啟
overview/OverviewViewModel.kt
。在_status
屬性宣告的正下方,新增類型為MutableLiveData
的可變動屬性_photos
,其可儲存單個MarsPhoto
物件。
private val _photos = MutableLiveData<MarsPhoto>()
依要求匯入 com.example.android.marsphotos.network.MarsPhoto
。
- 在
_photos
宣告的正下方,新增類型為LiveData<MarsPhoto>
的公開支援欄位photos
。
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。
通常您會使用以下程式碼在 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
- 在
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 的範圍函式之一,可讓您在物件環境內執行程式碼區塊。Kotlin 中有五個範圍函式,詳情請參閱說明文件。
使用方式:
let
的用途是根據呼叫鏈的結果叫用一或多個函式。
let
函式和安全呼叫運算子 (?.
) 的用途是對物件執行空值安全運算。在這種情況下,只有在物件不是空值時,系統才會執行 let
程式碼區塊。
- 在
bindImage()
函式中,使用安全呼叫運算子將let{}
區塊新增至imgUrl
引數。
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)
}
}
更新版面配置和片段
在上一節中,您使用了 Coil 圖片庫載入圖片。接著使用新屬性更新 ImageView
,以顯示單張圖片,即可在螢幕上顯示圖片。
稍後,在程式碼研究室中,您將使用 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>
- 在
ImageView
元素中新增app:imageUrl
屬性,即可使用新的圖片載入繫結轉接器。請注意,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)
}
}
}
- 執行應用程式。系統可能會在 Coil 下載並顯示屬性圖片時,暫時顯示載入圖片,具體情況視網路連線速度而定。但即使您關閉網路,此刻仍不會顯示毀損的圖片圖示。此問題會在程式碼研究室的最後一項工作中加以修正。
- 還原您在
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
新增了名為 _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
會將資料版面配置為可捲動的格線,如下所示。
從設計的角度來看,格線版面配置最適合用於可透過圖示或圖片表示的清單,例如火星相片瀏覽應用程式中的清單。
格線版面配置的項目排列方式
格線版面配置透過由列和欄組成的格線排列項目。假設您預設為垂直捲動,則每一列中的每個項目都會占用一個「跨距」。一個項目可以占用多個跨距。在下列範例中,一個跨距等同於一欄的寬度 (3)。
在以下兩個範例中,每列由三個跨距組成。根據預設,GridLayoutManager
會將每個項目放置在一個跨距中,直到達到您指定的跨距計數。達到跨距計數時,系統會換行到下一行。
新增 Recyclerview
在這個步驟中,您需要變更應用程式的版面配置,改用採用格線版面配置的回收器檢視畫面,而非單張圖片檢視畫面。
- 開啟
layout/grid_view_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」檢視畫面後,系統應會顯示類似下方螢幕截圖的預覽畫面。這個預覽畫面看起來不像火星相片,但您可以從中查看 recyclerview 格線版面配置的外觀。對於
recyclerview
中每個格線項目,其預覽皆會顯示邊框間距和grid_view_item
版面配置。
- 根據 Material Design 規範,清單頂端、底部和側邊應保留
8dp
的空間,而項目之間應保留4dp
的空間。搭配運用fragment_overview.xml
版面配置和grid_view_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" />
新增相片格線轉接器
現在,RecyclerView
在 fragment_overview
版面配置中採用格線版面配置。在這個步驟中,您可以透過 RecyclerView
轉接器,將從網路伺服器擷取的資料繫結至 RecyclerView
。
ListAdapter (重新整理程式)
ListAdapter
是 RecyclerView.Adapter
類別的子類別,用來在 RecyclerView
中顯示清單資料,包括在背景執行緒上計算清單之間的差異。
在此應用程式中,您將在 ListAdapter.
中使用 DiffUtil
實作。使用 DiffUtil
的優勢在於,每當新增、移除或變更 RecyclerView
中的某些項目時,系統不會重新整理整個清單。系統只會重新整理已變更的項目。
將 ListAdapter
新增至您的應用程式。
- 在
overview
套件中,建立名為PhotoGridAdapter
的 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
類別、紅色燈泡,然後從下拉式選單中選取「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")
}
實作 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
物件會將DiffUtil.ItemCallback
擴充為要比較的一般物件類型:MarsPhoto
。您需要在這項實作中比較兩張火星相片物件。
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
}
請確認您可以正常編譯並執行應用程式,但模擬器顯示空白畫面。recyclerview 已準備就緒,但尚未收到任何資料,您將在接下來的步驟中實作這部分內容。
新增繫結轉接器並連結各個部分
在這個步驟中,您將使用 BindingAdapter
來初始化包含 MarsPhoto
物件清單的 PhotoGridAdapter
。若使用 BindingAdapter
設定 RecyclerView
資料,資料繫結將自動監控 LiveData
的 MarsPhoto
物件清單。當 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
。接著將app:listData
屬性新增至RecyclerView
元素,然後使用資料繫結將屬性設為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 圖片庫的圖片可繪項目,用於在系統出現網路錯誤或無法擷取圖片時顯示。
恭喜您,就快完成了!在接下來的最後一項工作中,您需要在應用程式中新增更多錯誤處理機制,進一步改善使用者體驗。
5. 在 RecyclerView 中新增錯誤處理機制
MarsPhotos 應用程式會將無法擷取的圖片顯示為無效圖片圖示。但是如果沒有網路連線,應用程式會顯示空白畫面。您將在接下來的步驟中驗證空白畫面。
- 開啟裝置或模擬器上的飛航模式。透過 Android Studio 執行應用程式。請注意空白畫面。
這無法提供良好的使用者體驗。在這項工作中,您需要新增基本的錯誤處理機制,讓使用者更好地瞭解發生的情況。如果未連線到網際網路,應用程式將顯示連線錯誤圖示;而在擷取 MarsPhoto
清單時,應用程式會顯示載入動畫。
在 ViewModel 中新增狀態
在這項工作中,您會在 OverviewViewModel
中建立屬性來代表網路要求的狀態。需要考慮的狀態有三種:載入、成功和故障。當您等待資料時,畫面會顯示載入狀態。系統成功從網路服務擷取資料時會顯示成功狀態。出現網路或連線錯誤時會顯示故障狀態。
Kotlin 中的列舉類別
如要在應用程式中表示這三個狀態,請使用 enum
。enum
是列舉的縮寫,表示集合中所有項目已排序的清單。每個 enum
常數都是 enum
類別的一個物件。
在 Kotlin 中,enum
這種資料類型可容納一組常數。其定義方式是在類別定義前方加上關鍵字 enum
,如下所示。列舉常數會以半形逗號分隔。
定義:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
使用方式:
var direction : 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
設為空白清單。這項操作可清除回收器檢視畫面。
} 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
。
- 如要測試上述程式碼,請在模擬器或裝置上開啟飛航模式,模擬網路連線錯誤。接著編譯並執行應用程式,請注意,畫面上會顯示錯誤圖片:
- 輕觸返回按鈕即可關閉應用程式,並關閉飛航模式。使用最近畫面可返回應用程式。當應用程式查詢網路服務時,圖片開始載入之前,畫面上可能會極短暫地顯示載入旋轉圖示,具體視您的網路連線速度而定。
恭喜您完成本程式碼研究室,並建構了 MarsPhotos 應用程式!現在就與親朋好友分享真實的火星圖片,炫耀一下您的應用程式吧。
6. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方顯示的專案中。使用 main 分支版本提取或下載程式碼。
如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:
取得程式碼
- 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
7. 匯總
8. 瞭解詳情
Android 開發人員說明文件:
其他: