1. 事前準備
簡介
在先前的程式碼研究室中,您已瞭解如何使用存放區模式從 Web 服務取得資料,並將回應剖析為 Kotlin 物件。在本程式碼研究室中,您可以在該知識的基礎上,瞭解如何從網址載入並顯示相片。您也可以回顧如何建構 LazyVerticalGrid
,並在總覽頁面用以顯示圖片格線。
必要條件
- 瞭解如何從 REST Web 服務擷取 JSON,以及如何使用 Retrofit 和 Gson 程式庫將資料剖析為 Kotlin 物件
- 瞭解 REST Web 服務
- 熟悉 Android 架構元件,例如資料層和存放區
- 瞭解依附元件插入程序
- 瞭解
ViewModel
和ViewModelProvider.Factory
- 瞭解應用程式的協同程式實作
- 瞭解存放區模式
課程內容
- 如何使用 Coil 程式庫從網址載入及顯示圖片。
- 如何使用
LazyVerticalGrid
顯示圖片格線。 - 如何處理圖片下載及顯示時出現的潛在錯誤。
建構項目
- 修改 Mars Photos 應用程式以便取得火星資料中的圖片網址,並使用 Coil 載入及顯示該圖片。
- 在應用程式中新增載入動畫和錯誤圖示。
- 為應用程式加入狀態和錯誤處理機制。
軟硬體需求
- 電腦需搭載新版網路瀏覽器,例如最新版 Chrome
- 包含 REST Web 服務的 Mars Photos 應用程式範例程式碼
2. 應用程式總覽
在本程式碼研究室中,您將繼續使用先前程式碼研究室的 Mars Photos 應用程式。Mars Photos 應用程式會連上 Web 服務,擷取並顯示使用 Gson 獲取的 Kotlin 物件數量。這些 Kotlin 物件包含網址,可導向 NASA 火星探測車拍攝的真實火星表面相片。
您在本程式碼研究室中建構的應用程式版本,是以圖片格線模式顯示火星相片。這些圖片取自應用程式從 Web 服務擷取的資料。應用程式會使用 Coil 程式庫來載入並顯示圖片,並使用 LazyVerticalGrid
建立圖片的格線版面配置。此外,應用程式還會顯示錯誤訊息,妥善處理網路錯誤。
取得範例程式碼
如要開始使用,請先下載範例程式碼:
或者,您也可以複製 GitHub 存放區的程式碼:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git $ cd basic-android-kotlin-compose-training-mars-photos $ git checkout coil-starter
您可以瀏覽 Mars Photos
GitHub 存放區中的程式碼。
3. 顯示下載的圖片
想要顯示某個網址的相片,聽起來可能很簡單,但其實需要執行一些軟體工程才能達成。圖片必須經過下載、內部儲存 (快取)、解碼壓縮格式,才能供 Android 使用。您可以將圖片快取到記憶體內快取/儲存空間快取,或同時存放在這兩個地方。系統只會在低優先順序的背景執行緒中採取這些動作,讓 UI 保持回應。此外,為獲得最佳網路和 CPU 效能,建議您一次擷取多張圖片並解碼。
好消息是,您可以使用由社群開發的資料庫 Coil 下載、緩衝處理、解碼及快取圖片。如果不使用 Coil,工作將會更多。
Coil 基本上需要下列兩項:
- 要載入並顯示的圖片網址。
- 實際顯示該圖片的
AsyncImage
可組合函式。
在這項工作中,您會瞭解如何使用 Coil 顯示來自「火星」Web 服務的單張圖片。Web 服務會傳回一個相片清單,您將顯示清單上的第一張火星相片。下圖是前後對照的螢幕截圖:
新增 Coil 依附元件
- 開啟「新增存放區和手動 DI」程式碼研究室中的 Mars Photos 解決方案應用程式。
- 執行應用程式,確認應用程式會顯示擷取的火星相片數量。
- 開啟「build.gradle.kts (Module :app)」。
- 在
dependencies
區段,為 Coil 程式庫新增此行內容:
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
透過 Coil 說明文件頁面查看及更新最新版本的程式庫。
- 按一下「Sync Now」,使用新依附元件重新建構專案。
顯示圖片網址
在這個步驟中,您可以擷取並顯示第一張火星相片的網址。
- 在
ui/screens/MarsViewModel.kt
內getMarsPhotos()
方法的try
區塊中,找出可將從 Web 服務擷取到的資料設為listResult
的那一行。
// No need to copy, code is already present
try {
val listResult = marsPhotosRepository.getMarsPhotos()
//...
}
- 將
listResult
變更為result
,把擷取到的第一張火星相片指派給新變數result
後,更新這一行內容。請在索引0
處指派第一個相片物件。
try {
val result = marsPhotosRepository.getMarsPhotos()[0]
//...
}
- 在下一行中,將傳遞至
MarsUiState.Success()
函式呼叫的參數更新為以下程式碼中的字串。請使用新屬性 (非listResult
中) 的資料,顯示相片result
的第一個圖片網址。
try {
...
MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}
現在,完整的 try
區塊程式碼如下所示:
marsUiState = try {
val result = marsPhotosRepository.getMarsPhotos()[0]
MarsUiState.Success(
" First Mars image URL : ${result.imgSrc}"
)
}
- 執行應用程式。現在,
Text
可組合函式會顯示第一張火星相片的網址。下一節將說明如何讓應用程式顯示這個網址中的圖片。
新增 AsyncImage
可組合函式
在這個步驟中,您將新增 AsyncImage
可組合函式來載入並顯示單張火星相片。AsyncImage
這個可組合函式能以非同步方式執行圖片要求,並轉譯結果。
// Example code, no need to copy over
AsyncImage(
model = "https://android.com/sample_image.jpg",
contentDescription = null
)
model
引數可以是 ImageRequest.data
值或 ImageRequest
本身。在上述範例中,您指派了 ImageRequest.data
值,也就是圖片網址 "https://android.com/sample_image.jpg"
。以下範例程式碼說明如何將 ImageRequest
指派給 model
。
// Example code, no need to copy over
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.placeholder),
contentDescription = stringResource(R.string.description),
contentScale = ContentScale.Crop,
modifier = Modifier.clip(CircleShape)
)
AsyncImage
支援與標準 Image 可組合函式相同的引數。此外,該函式也支援設定 placeholder
/error
/fallback
繪圖工具和 onLoading
/onSuccess
/onError
回呼。上述範例程式碼會採用圓形裁剪和交叉漸變的方式載入圖片,並設定預留位置。
contentDescription
會設定無障礙服務說明文字,解釋這張圖片含意。
在程式碼中新增 AsyncImage
可組合函式,顯示擷取到的第一張火星相片。
- 在
ui/screens/HomeScreen.kt
中,新增名為MarsPhotoCard()
的新可組合函式,該函式採用MarsPhoto
和Modifier
。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
- 在
MarsPhotoCard()
可組合函式中,新增AsyncImage()
函式,如下所示:
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.platform.LocalContext
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
在上述程式碼中,您可以使用圖片網址 (photo.imgSrc
) 建構 ImageRequest
,然後將其傳遞至 model
引數。請使用 contentDescription
設定無障礙閱讀器的文字。
- 將
crossfade(true)
新增至ImageRequest
,在要求成功完成時啟用交叉漸變動畫。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
- 更新
HomeScreen
可組合函式,在要求成功完成時顯示MarsPhotoCard
(而非ResultScreen
) 可組合函式。您將在下個步驟中修正類型不符錯誤。
@Composable
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier.fillMaxSize())
else -> ErrorScreen(modifier = modifier.fillMaxSize())
}
}
- 在
MarsViewModel.kt
檔案中更新MarsUiState
介面,接受MarsPhoto
物件,而非String
。
sealed interface MarsUiState {
data class Success(val photos: MarsPhoto) : MarsUiState
//...
}
- 更新
getMarsPhotos()
函式,將第一個火星相片物件傳遞至MarsUiState.Success()
。請刪除result
變數。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
- 執行應用程式,確認應用程式顯示單張火星圖片。
- 火星相片並未填滿整個畫面。如要填滿畫面上的可用空間,請在
AsyncImage
內的HomeScreen.kt
中,將contentScale
設為ContentScale.Crop
。
import androidx.compose.ui.layout.ContentScale
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = modifier,
)
}
- 執行應用程式,確認圖片在水平和垂直方向均填滿畫面。
新增載入和錯誤圖片
您可在載入圖片時顯示預留位置圖片,改善應用程式的使用者體驗。如果因圖片檔遺失或損毀等問題而造成載入失敗,您還可以顯示錯誤圖片。在本節中,您會使用 AsyncImage
新增錯誤和預留位置圖片。
- 開啟
res/drawable/ic_broken_image.xml
,然後按一下右側的「Design」或「Split」分頁標籤。如果出現錯誤圖片,請使用內建圖示庫中的毀損圖片圖示。此向量可繪項目使用android:tint
屬性,將圖示變為灰色。
- 開啟
res/drawable/loading_img.xml
。這一可繪項目是個動畫,可圍繞聚焦點旋轉圖片可繪項目loading_img.xml
(預覽畫面不會顯示動畫)。
- 返回
HomeScreen.kt
檔案。在MarsPhotoCard
可組合函式中,更新對AsyncImage()
的呼叫,新增error
和placeholder
屬性,如以下程式碼所示:
import androidx.compose.ui.res.painterResource
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
// ...
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
// ...
)
}
此程式碼會設定載入時要使用的預留位置載入圖片 (loading_img
可繪項目),還會設定圖片載入失敗時要使用的圖片 (ic_broken_image
可繪項目)。
現在,完整的 MarsPhotoCard
可組合函式會如以下程式碼所示:
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop
)
}
- 執行應用程式。系統可能會在 Coil 下載並顯示屬性圖片時,暫時顯示載入圖片,具體情況視網路連線速度而定。但即使您關閉網路,此刻仍不會顯示毀損圖片圖示。此問題會在程式碼研究室的最後一項工作中修正。
4. 透過 LazyVerticalGrid 顯示圖片格線
現在,您的應用程式會從網際網路載入火星相片,也就是第一個 MarsPhoto
清單項目。您已使用火星相片資料中的圖片網址填入 AsyncImage
。然而,我們的目標是讓應用程式顯示圖片格線。在這項工作中,您會使用 LazyVerticalGrid
搭配格線版面配置管理工具來顯示圖片格線。
Lazy 格線
LazyVerticalGrid 和 LazyHorizontalGrid 可組合函式支援在格線中顯示項目。Lazy 垂直格線會在橫跨多欄的垂直捲動式容器中顯示項目,而 Lazy 水平格線在水平軸上也會如此。
從設計的角度來看,如要將火星相片顯示為圖示或圖片,最好的辦法就是使用格狀版面配置。
LazyVerticalGrid
中的 columns
參數和 LazyHorizontalGrid
中的 rows
參數可控管儲存格轉換為欄或列的方式。以下範例程式碼將以格狀檢視方式顯示項目,並使用 GridCells.Adaptive
將每欄的寬度設為至少 128.dp
:
// Sample code - No need to copy over
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
LazyVerticalGrid
可讓您指定項目寬度,然後格線會盡可能容納更多的欄。計算完欄數後,格線會在各欄之間平均分配剩餘寬度。這種自動調整尺寸的方式特別適合用來顯示不同螢幕尺寸的項目組合。
在本程式碼研究室中,如要顯示火星相片,請使用 LazyVerticalGrid
可組合函式搭配 GridCells.Adaptive
,每欄寬度設為 150.dp
。
項目鍵
當使用者捲動格線 (LazyColumn
中的 LazyRow
) 時,清單項目位置就會變更。不過,由於螢幕方向改變或項目新增/移除的關係,使用者可能會找不到原先在列中的捲動位置。有了項目鍵,就能根據鍵來保留捲動位置。
只要提供鍵,您就能讓 Compose 正確重新排序。例如,如果您的項目包含一個已記下的狀態,當您設定鍵之後,即可讓 Compose 在項目的位置改變時,連同狀態一併移動項目。
新增 LazyVerticalGrid
請新增可組合函式,以垂直格線顯示火星相片清單。
- 在
HomeScreen.kt
檔案中,建立名為PhotosGridScreen()
的新可組合函式,該函式將MarsPhoto
和modifier
清單做為引數。
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
- 在
PhotosGridScreen
可組合函式內,新增含有下列參數的LazyVerticalGrid
。
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
modifier = modifier.padding(horizontal = 4.dp),
contentPadding = contentPadding,
) {
}
}
- 如要新增項目清單,請於
LazyVerticalGrid
lambda 內呼叫items()
函式,傳入MarsPhoto
清單和採用photo.id
形式的項目鍵。
import androidx.compose.foundation.lazy.grid.items
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
// ...
) {
items(items = photos, key = { photo -> photo.id }) {
}
}
}
- 如要新增單一清單項目顯示的內容,請定義
items
lambda 運算式,呼叫MarsPhotoCard
傳入photo
。
items(items = photos, key = { photo -> photo.id }) {
photo -> MarsPhotoCard(photo)
}
- 更新
HomeScreen
可組合函式,在成功完成要求時顯示PhotosGridScreen
(而非MarsPhotoCard
) 可組合函式。
when (marsUiState) {
// ...
is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
// ...
}
- 在
MarsViewModel.kt
檔案中更新MarsUiState
介面,接受MarsPhoto
物件清單,而非單一MarsPhoto
。PhotosGridScreen
可組合函式接受MarsPhoto
物件清單。
sealed interface MarsUiState {
data class Success(val photos: List<MarsPhoto>) : MarsUiState
//...
}
- 在
MarsViewModel.kt
檔案中更新getMarsPhotos()
函式,將火星相片物件清單傳遞至MarsUiState.Success()
。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
- 執行應用程式。
請注意,每張相片周圍都沒有邊框間距,而且每張相片的顯示比例皆不同。您可以新增 Card
可組合函式來修正這些問題。
新增資訊卡可組合函式
- 在
HomeScreen.kt
檔案的MarsPhotoCard
可組合函式中,於AsyncImage
四周新增高度為8.dp
的Card
。將modifier
引數指派給Card
可組合函式。
import androidx.compose.material.Card
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
}
}
- 如要修正顯示比例,請在
PhotosGridScreen()
中更新MarsPhotoCard()
的修飾符。
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
LazyVerticalGrid(
//...
) {
items(items = photos, key = { photo -> photo.id }) { photo ->
MarsPhotoCard(
photo,
modifier = modifier
.padding(4.dp)
.fillMaxWidth()
.aspectRatio(1.5f)
)
}
}
}
- 更新結果預覽畫面,即可預覽
PhotosGridScreen()
。請以空白的圖片網址模擬資料。
@Preview(showBackground = true) @Composable fun PhotosGridScreenPreview() { MarsPhotosTheme { val mockData = List(10) { MarsPhoto("$it", "") } PhotosGridScreen(mockData) } }
由於模擬資料包含空白網址,系統會在相片格線預覽畫面中顯示載入中的圖片。
- 執行應用程式。
- 應用程式執行期間,請開啟飛航模式。
- 在模擬器中捲動圖片。尚未載入的圖片會顯示為無效圖片圖示。這是您傳遞至 Coil 圖片庫的圖片可繪項目,可在系統出現網路錯誤或無法擷取圖片時顯示。
太棒了!您已在模擬器或裝置上開啟飛航模式,模擬網路連線錯誤。
5. 新增重試動作
在本節中,您將新增重試動作按鈕,並在使用者點選該按鈕時擷取相片。
- 在錯誤畫面中新增按鈕。請在
HomeScreen.kt
檔案中更新ErrorScreen()
可組合函式,加入retryAction
lambda 參數和按鈕。
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
Column(
// ...
) {
Image(
// ...
)
Text(//...)
Button(onClick = retryAction) {
Text(stringResource(R.string.retry))
}
}
}
查看預覽畫面
- 更新
HomeScreen()
可組合函式,傳入重試 lambda。
@Composable
fun HomeScreen(
marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
when (marsUiState) {
//...
is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
}
}
- 在
ui/theme/MarsPhotosApp.kt
檔案中更新HomeScreen()
函式呼叫,將retryAction
lambda 參數設為marsViewModel::getMarsPhotos
。這麼做會從伺服器擷取火星相片。
HomeScreen(
marsUiState = marsViewModel.marsUiState,
retryAction = marsViewModel::getMarsPhotos
)
6. 更新 ViewModel 測試
MarsUiState
和 MarsViewModel
現在包含相片清單,而非單張相片。在目前的狀態中,MarsViewModelTest
會預期 MarsUiState.Success
資料類別包含字串屬性。因此,系統不會編譯測試。您需更新 marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
測試,宣告 MarsViewModel.marsUiState
等同於包含相片清單的 Success
狀態。
- 開啟
rules/MarsViewModelTest.kt
檔案。 - 在
marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
測試中,修改assertEquals()
函式呼叫,比較Success
狀態 (將假相片清單傳遞至相片參數) 與marsViewModel.marsUiState
。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest {
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
assertEquals(
MarsUiState.Success(FakeDataSource.photosList),
marsViewModel.marsUiState
)
}
現在系統即可編譯、執行並通過測試!
7. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 Git 指令:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。
如要查看本程式碼研究室的解決方案程式碼,請前往 GitHub。
8. 結語
恭喜您完成本程式碼研究室,並建構了 Mars Photos 應用程式!現在就與親朋好友分享真實的火星相片,展現一下您的酷炫應用程式吧。
記得使用 #AndroidBasics,在社群媒體分享您的作品!
9. 瞭解詳情
Android 開發人員說明文件:
其他: