Tải và hiển thị hình ảnh từ Internet

1. Trước khi bắt đầu

Giới thiệu

Trong các lớp học lập trình trước, bạn đã tìm hiểu cách lấy dữ liệu qua một dịch vụ web bằng cách sử dụng mẫu kho lưu trữ và phân tích cú pháp phản hồi thành một đối tượng Kotlin. Trong lớp học lập trình này, bạn sẽ phát huy kiến thức đó để tải và hiển thị ảnh lấy từ một URL. Bạn cũng sẽ ôn lại cách tạo và sử dụng LazyVerticalGrid để trình bày hình ảnh theo bố cục lưới trên trang tổng quan.

Điều kiện tiên quyết

  • Có kiến thức về cách truy xuất JSON qua dịch vụ web REST và việc phân tích cú pháp dữ liệu đó thành đối tượng trong Kotlin bằng cách dùng thư viện RetrofitGson
  • Có kiến thức về dịch vụ web REST
  • Quen thuộc với bộ thành phần cấu trúc Android, chẳng hạn như lớp dữ liệu và kho lưu trữ
  • Có kiến thức về chèn phần phụ thuộc
  • Có kiến thức về ViewModelViewModelProvider.Factory
  • Có kiến thức về cách triển khai coroutine cho ứng dụng
  • Có kiến thức về mẫu kho lưu trữ

Kiến thức bạn sẽ học được

  • Cách sử dụng thư viện Coil để tải và hiển thị hình ảnh lấy từ một URL.
  • Cách sử dụng LazyVerticalGrid để hiển thị hình ảnh theo bố cục lưới.
  • Cách xử lý các lỗi có thể xảy ra khi tải và hiển thị hình ảnh.

Sản phẩm bạn sẽ tạo ra

  • Sửa đổi ứng dụng Mars Photos (Ảnh sao Hoả) để lấy URL hình ảnh từ dữ liệu về sao Hoả, sau đó sử dụng thư viện Coil để tải và hiển thị hình ảnh đó.
  • Thêm ảnh động thể hiện trạng thái đang tải và biểu tượng lỗi vào ứng dụng.
  • Thêm trạng thái và mã xử lý lỗi vào ứng dụng.

Bạn cần có

  • Một máy tính sử dụng trình duyệt web hiện đại (chẳng hạn như trình duyệt Chrome phiên bản mới nhất)
  • Mã khởi đầu cho ứng dụng Mars Photos cùng với dịch vụ web REST

2. Tổng quan về ứng dụng

Trong lớp học lập trình này, bạn sẽ tiếp tục làm việc với ứng dụng Mars Photos từ lớp học lập trình trước. Ứng dụng Mars Photos kết nối với một dịch vụ web để truy xuất và hiển thị số lượng đối tượng Kotlin truy xuất được bằng Gson. Các đối tượng Kotlin này chứa URL của ảnh thật về bề mặt sao Hoả, được chụp từ Thiết bị thám hiểm sao Hoả của NASA.

a59e55909b6e9213.png

Phiên bản ứng dụng mà bạn tạo trong lớp học lập trình này sẽ hiển thị ảnh chụp sao Hoả theo bố cục lưới. Những hình ảnh này nằm trong dữ liệu mà ứng dụng của bạn truy xuất từ dịch vụ web. Ứng dụng của bạn sẽ sử dụng thư viện Coil để tải và hiển thị hình ảnh, đồng thời sử dụng LazyVerticalGrid để tạo bố cục lưới cho ảnh. Ứng dụng của bạn cũng sẽ xử lý lỗi mạng một cách linh hoạt bằng cách hiển thị thông báo lỗi.

68f4ff12cc1e2d81.png

Lấy mã khởi đầu

Để bắt đầu, hãy tải mã khởi đầu xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:

$ 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

Bạn có thể xem mã này trong kho lưu trữ GitHub Mars Photos.

3. Hiển thị hình ảnh đã tải xuống

Việc hiển thị hình ảnh lấy từ URL nghe có vẻ đơn giản, nhưng có khá nhiều kỹ thuật cần áp dụng để hình ảnh hoạt động tốt. Hình ảnh phải được tải xuống, lưu trữ nội bộ (vào bộ nhớ đệm) và giải mã từ định dạng nén thành hình ảnh mà Android có thể sử dụng. Bạn có thể lưu hình ảnh vào bộ nhớ đệm của bộ nhớ trong, bộ nhớ đệm trên thành phần lưu trữ hoặc cả hai. Toàn bộ quá trình này phải diễn ra trong luồng có mức độ ưu tiên thấp ở chế độ nền, để giao diện người dùng vẫn có thể phản hồi. Ngoài ra, để có chất lượng kết nối mạng và hiệu suất CPU tốt nhất, bạn nên tìm nạp và giải mã nhiều hình ảnh cùng một lúc.

May mắn là bạn có thể sử dụng một thư viện do cộng đồng phát triển có tên là Coil để tải xuống, lưu vào bộ đệm, giải mã và lưu hình ảnh vào bộ nhớ đệm. Nếu không dùng Coil, bạn sẽ phải làm nhiều việc hơn.

Về cơ bản, Coil cần hai thứ:

  • URL của hình ảnh bạn muốn tải và hiển thị.
  • Một thành phần kết hợp AsyncImage để hiển thị hình ảnh đó.

Trong nhiệm vụ này, bạn sẽ tìm hiểu cách sử dụng Coil để hiển thị một hình ảnh từ dịch vụ web về sao Hoả. Bạn cho hiện ảnh chụp sao Hoả nằm đầu tiên trong danh sách mà dịch vụ web trả về. Các hình ảnh sau hiển thị ảnh chụp màn hình trước và sau:

a59e55909b6e9213.png 1b670f284109bbf5.png

Thêm phần phụ thuộc từ Coil

  1. Mở ứng dụng giải pháp Mars Photos từ lớp học lập trình Thêm kho lưu trữ và DI thủ công.
  2. Chạy ứng dụng này để xác nhận rằng ứng dụng hiển thị số lượng ảnh chụp sao Hoả đã truy xuất.
  3. Mở build.gradle.kts (Module :app).
  4. Trong phần dependencies, hãy thêm dòng này để gọi thư viện Coil:
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")

Kiểm tra và cập nhật lên phiên bản mới nhất của thư viện này trên trang tài liệu về Coil.

  1. Nhấp vào Sync Now (Đồng bộ hoá ngay) để tạo lại dự án với phần phụ thuộc mới.

Hiển thị URL hình ảnh

Ở bước này, bạn truy xuất và hiển thị URL của ảnh chụp sao Hoả đầu tiên.

  1. Trong ui/screens/MarsViewModel.kt, bên trong phương thức getMarsPhotos(), bên trong khối try, hãy tìm dòng mã có vai trò thiết lập dữ liệu được truy xuất từ dịch vụ web thành listResult.
// No need to copy, code is already present
try {
   val listResult = marsPhotosRepository.getMarsPhotos()
   //...
}
  1. Cập nhật dòng mã này bằng cách thay đổi listResult thành result và gán ảnh chụp sao Hoả nằm đầu tiên trong danh sách được truy xuất vào biến result mới. Gán đối tượng ảnh đầu tiên tại chỉ mục 0.
try {
   val result = marsPhotosRepository.getMarsPhotos()[0]
   //...
}
  1. Trong dòng mã tiếp theo, hãy cập nhật tham số được truyền đến lệnh gọi hàm MarsUiState.Success() thành string trong mã sau. Sử dụng dữ liệu từ thuộc tính mới thay vì listResult. Hiển thị URL của hình ảnh đầu tiên trong ảnh result.
try {
   ...
   MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}

Khối try hoàn chỉnh giờ đây có dạng như mã sau:

marsUiState = try {
   val result = marsPhotosRepository.getMarsPhotos()[0]
   MarsUiState.Success(
       "   First Mars image URL : ${result.imgSrc}"
   )
}
  1. Chạy ứng dụng. Thành phần kết hợp Text giờ đây hiển thị URL của ảnh chụp sao Hoả nằm đầu tiên trong danh sách. Phần tiếp theo mô tả cách để ứng dụng hiển thị hình ảnh trong URL này.

b5daaa892fe8dad7.png

Thêm thành phần kết hợp AsyncImage

Ở bước này, bạn sẽ thêm hàm có khả năng kết hợp AsyncImage để tải và hiển thị một ảnh chụp sao Hoả. AsyncImage là một thành phần kết hợp thực thi yêu cầu hình ảnh một cách không đồng bộ và hiển thị kết quả.

// Example code, no need to copy over
AsyncImage(
    model = "https://android.com/sample_image.jpg",
    contentDescription = null
)

Đối số model có thể là giá trị ImageRequest.data hoặc chính ImageRequest. Trong ví dụ trước, bạn gán giá trị ImageRequest.data, tức là URL hình ảnh "https://android.com/sample_image.jpg". Đoạn mã ví dụ sau đây cho thấy cách gán chính ImageRequest cho 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 hỗ trợ các đối số tương tự như thành phần kết hợp Hình ảnh chuẩn. Ngoài ra, bạn cũng có thể cài đặt trình vẽ placeholder/error/fallback và lệnh gọi lại onLoading/onSuccess/onError. Mã trong ví dụ tải hình ảnh cùng một ảnh cắt theo hình tròn và ảnh động chuyển đổi, rồi đặt một phần giữ chỗ.

contentDescription đặt văn bản mà các dịch vụ hỗ trợ tiếp cận sử dụng để mô tả nội dung của hình ảnh này.

Thêm thành phần kết hợp AsyncImage vào mã để hiển thị ảnh chụp sao Hoả đầu tiên được truy xuất.

  1. Trong ui/screens/HomeScreen.kt, hãy thêm một hàm có khả năng kết hợp mới có tên là MarsPhotoCard(). Hàm này sẽ chứa MarsPhotoModifier.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
  1. Bên trong hàm có khả năng kết hợp MarsPhotoCard(), hãy thêm hàm AsyncImage() như sau:
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()
    )
}

Trong mã trước, bạn tạo ImageRequest bằng cách sử dụng và truyền URL hình ảnh (photo.imgSrc) đến đối số model. Bạn dùng contentDescription để đặt văn bản cho trình đọc hỗ trợ tiếp cận.

  1. Thêm crossfade(true) vào ImageRequest để bật ảnh động chuyển đổi khi yêu cầu hoàn tất thành công.
@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()
    )
}
  1. Cập nhật thành phần kết hợp HomeScreen để hiển thị thành phần kết hợp MarsPhotoCard thay vì thành phần kết hợp ResultScreen khi yêu cầu hoàn tất thành công. Trong bước tiếp theo, bạn sẽ khắc phục lỗi kiểu dữ liệu không khớp.
@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())
    }
}
  1. Trong tệp MarsViewModel.kt, hãy cập nhật giao diện MarsUiState để chấp nhận đối tượng MarsPhoto thay vì String.
sealed interface MarsUiState {
    data class Success(val photos: MarsPhoto) : MarsUiState
    //...
}
  1. Cập nhật hàm getMarsPhotos() để truyền đối tượng ảnh chụp sao Hoả đầu tiên đến MarsUiState.Success(). Xoá biến result.
marsUiState = try {
    MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
  1. Chạy ứng dụng và xác nhận rằng ứng dụng hiển thị một ảnh chụp sao Hoả.

d4421a2458f38695.png

  1. Ảnh chụp sao Hoả không lấp đầy toàn bộ màn hình. Để lấp đầy không gian có sẵn trên màn hình, trong HomeScreen.kt bên trong AsyncImage, hãy đặt contentScale thành 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,
   )
}
  1. Chạy ứng dụng và xác nhận rằng hình ảnh sẽ lấp đầy màn hình cả chiều ngang và chiều dọc.

1b670f284109bbf5.png

Thêm hình ảnh thể hiện lỗi và trạng thái đang tải

Bạn có thể cải thiện trải nghiệm người dùng trong ứng dụng bằng cách hiển thị hình ảnh phần giữ chỗ trong khi tải hình ảnh. Bạn cũng có thể hiển thị hình ảnh lỗi nếu không tải được do sự cố, chẳng hạn như tệp hình ảnh bị thiếu hoặc bị hỏng. Trong phần này, bạn sẽ thêm cả hình ảnh lỗi và hình ảnh giữ chỗ bằng cách sử dụng AsyncImage.

  1. Mở res/drawable/ic_broken_image.xml và nhấp vào thẻ Design (Thiết kế) hoặc Split (Tách) ở bên phải. Đối với hình ảnh lỗi, hãy dùng biểu tượng hình ảnh bị hỏng có trong thư viện biểu tượng tích hợp. Vectơ vẽ được này sử dụng thuộc tính android:tint để làm biểu tượng có màu xám.

70e008c63a2a1139.png

  1. Mở res/drawable/loading_img.xml. Thành phần có thể vẽ này là một hiệu ứng động làm cho hình ảnh có thể vẽ (loading_img.xml) xoay xung quanh điểm giữa. (Bạn không thấy ảnh động trong bản xem trước).

92a448fa23b6d1df.png

  1. Quay lại tệp HomeScreen.kt. Trong thành phần kết hợp MarsPhotoCard, hãy cập nhật lệnh gọi thành AsyncImage() để thêm các thuộc tính errorplaceholder, như minh hoạ trong đoạn mã dưới đây:
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),
        // ...
    )
}

Mã này đặt hình ảnh phần giữ chỗ trong quá trình tải để sử dụng trong khi tải (đối tượng có thể vẽ loading_img). Mã này cũng đặt hình ảnh để sử dụng nếu hình ảnh không tải được (đối tượng có thể vẽ ic_broken_image).

Thành phần kết hợp MarsPhotoCard hoàn chỉnh giờ đây có dạng như mã sau:

@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
    )
}
  1. Chạy ứng dụng. Tuỳ thuộc vào tốc độ kết nối mạng, có thể bạn sẽ thấy biểu tượng đang tải trong giây lát khi Coil tải xuống và hiển thị hình ảnh thuộc tính. Tuy nhiên, bạn sẽ không thấy biểu tượng hình ảnh bị hỏng ngay cả khi tắt mạng – bạn sẽ khắc phục vấn đề đó trong nhiệm vụ cuối cùng của lớp học lập trình này.

d684b0e096e57643.gif

4. Hiển thị hình ảnh theo bố cục lưới thông qua LazyVerticalGrid

Ứng dụng của bạn đang tải ảnh chụp sao Hoả từ Internet, mục MarsPhoto đầu tiên trong danh sách. Bạn đã sử dụng URL hình ảnh từ dữ liệu về ảnh chụp sao Hoả đó để điền sẵn dữ liệu vào AsyncImage. Tuy nhiên, mục tiêu của ứng dụng là hiển thị lưới hình ảnh. Trong nhiệm vụ này, bạn sử dụng LazyVerticalGrid với trình quản lý bố cục Lưới để hiển thị lưới hình ảnh.

Lưới lazy

Các thành phần kết hợp LazyVerticalGridLazyHorizontalGrid hỗ trợ việc hiển thị các mục trong một lưới. Lưới dọc lazy hiển thị các mục thuộc lưới đó trong một vùng chứa có thể cuộn theo chiều dọc, kéo dài qua nhiều cột, còn lưới ngang lazy cũng hoạt động tương tự nhưng theo chiều ngang.

27680e208333ed5.png

Từ góc độ thiết kế, Bố cục lưới là phù hợp nhất để hiển thị ảnh sao Hoả dưới dạng biểu tượng hoặc hình ảnh.

Tham số columns trong LazyVerticalGrid và tham số rows trong LazyHorizontalGrid kiểm soát cách các ô được tạo thành cột hoặc hàng. Mã ví dụ dưới đây hiển thị các mục trong một lưới, sử dụng GridCells.Adaptive để đặt chiều rộng tối thiểu cho mỗi cột là 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 cho phép bạn chỉ định chiều rộng của các mục để lưới có thể vừa vặn với nhiều cột nhất có thể. Sau khi tính số lượng cột, lưới này sẽ phân bổ đều chiều rộng còn lại cho các cột. Phương pháp định cỡ thích ứng này đặc biệt hữu ích khi hiển thị các nhóm mục trên nhiều kích thước màn hình.

Trong lớp học lập trình này, để hiển thị ảnh chụp sao Hoả, bạn sử dụng thành phần kết hợp LazyVerticalGrid với GridCells.Adaptive, trong đó mỗi cột có chiều rộng là 150.dp.

Khoá mục

Khi người dùng cuộn qua lưới (LazyRow trong LazyColumn), vị trí của mục trong danh sách sẽ thay đổi. Tuy nhiên, do sự thay đổi hướng hoặc nếu các mục được thêm hoặc bị xoá, người dùng có thể mất vị trí cuộn trong hàng. Khoá mục giúp bạn duy trì vị trí cuộn dựa trên khoá.

Bằng cách cung cấp các khoá, bạn giúp Compose xử lý việc sắp xếp lại thứ tự một cách chính xác. Ví dụ: nếu mục của bạn chứa trạng thái đã nhớ, các khoá cài đặt sẽ cho phép Compose di chuyển trạng thái này cùng với mục khi vị trí của mục thay đổi.

Thêm LazyVerticalGrid

Thêm thành phần kết hợp để hiển thị danh sách ảnh sao Hoả theo dạng lưới dọc.

  1. Trong tệp HomeScreen.kt, hãy tạo một hàm có khả năng kết hợp mới có tên là PhotosGridScreen(). Hàm này sẽ lấy danh sách MarsPhotomodifier làm đối số.
@Composable
fun PhotosGridScreen(
    photos: List<MarsPhoto>,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
  1. Bên trong thành phần kết hợp PhotosGridScreen, hãy thêm LazyVerticalGrid với các tham số sau.
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,
   ) {
     }
}
  1. Để thêm danh sách các mục, bên trong hàm lambda LazyVerticalGrid, hãy gọi hàm items() truyền vào danh sách MarsPhoto và khoá mục là 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 }) {
       }
   }
}
  1. Để thêm nội dung mà một mục trong danh sách hiển thị, hãy xác định biểu thức lambda items. Gọi MarsPhotoCard, truyền vào photo.
items(items = photos, key = { photo -> photo.id }) {
   photo -> MarsPhotoCard(photo)
}
  1. Cập nhật thành phần kết hợp HomeScreen để hiển thị thành phần kết hợp PhotosGridScreen thay vì thành phần kết hợp MarsPhotoCard khi hoàn tất yêu cầu thành công.
when (marsUiState) {
       // ...
       is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
       // ...
}
  1. Trong tệp MarsViewModel.kt, hãy cập nhật giao diện MarsUiState để chấp nhận danh sách đối tượng MarsPhoto thay vì một MarsPhoto. Thành phần kết hợp PhotosGridScreen sẽ chấp nhận danh sách đối tượng MarsPhoto.
sealed interface MarsUiState {
    data class Success(val photos: List<MarsPhoto>) : MarsUiState
    //...
}
  1. Trong tệp MarsViewModel.kt, hãy cập nhật hàm getMarsPhotos() để truyền danh sách đối tượng ảnh chụp sao Hoả vào MarsUiState.Success().
marsUiState = try {
    MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
  1. Chạy ứng dụng.

2eaec198c56b5eed.png

Hãy lưu ý rằng không có khoảng đệm nào quanh mỗi ảnh và tỷ lệ khung hình thay đổi tuỳ theo ảnh. Bạn có thể thêm thành phần kết hợp Card để khắc phục những vấn đề này.

Thêm thành phần kết hợp thẻ

  1. Ở tệp HomeScreen.kt, trong thành phần kết hợp MarsPhotoCard, hãy thêm Card có độ nâng 8.dp xung quanh AsyncImage. Chỉ định đối số modifier cho thành phần kết hợp 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()
        )
    }
}
  1. Để chỉnh tỷ lệ khung hình, trong PhotosGridScreen(), hãy cập nhật đối tượng sửa đổi cho 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)
           )
       }
   }
}
  1. Cập nhật bản xem trước màn hình kết quả để xem trước PhotosGridScreen(). Dữ liệu mô phỏng có URL hình ảnh trống.
@Preview(showBackground = true)
@Composable
fun PhotosGridScreenPreview() {
   MarsPhotosTheme {
       val mockData = List(10) { MarsPhoto("$it", "") }
       PhotosGridScreen(mockData)
   }
}

Vì dữ liệu mô phỏng có URL trống, nên bạn sẽ thấy hình ảnh đang tải trong bản xem trước lưới ảnh.

Bản xem trước màn hình lưới ảnh với hình ảnh đang tải

  1. Chạy ứng dụng.

b56acd074ce0f9c7.png

  1. Khi ứng dụng đang chạy, hãy bật Chế độ trên máy bay.
  2. Cuộn hình ảnh trong trình mô phỏng. Hình ảnh chưa tải sẽ xuất hiện dưới dạng biểu tượng hình ảnh bị hỏng. Đây là hình ảnh có thể vẽ mà bạn đã truyền vào thư viện hình ảnh Coil để hiển thị trong trường hợp có lỗi mạng hoặc không tìm nạp được hình ảnh.

9b72c1d4206c7331.png

Tốt lắm! Bạn đã mô phỏng lỗi kết nối mạng bằng cách bật Chế độ trên máy bay trong trình mô phỏng hoặc thiết bị.

5. Thêm hành động thử lại

Trong phần này, bạn sẽ thêm một nút hành động để thử lại và truy xuất ảnh khi được nhấp vào.

60cdcd42bc540162.png

  1. Thêm một nút vào màn hình lỗi. Trong tệp HomeScreen.kt, hãy cập nhật thành phần kết hợp ErrorScreen() để thêm tham số lambda retryAction và nút.
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
    Column(
        // ...
    ) {
        Image(
            // ...
        )
        Text(//...)
        Button(onClick = retryAction) {
            Text(stringResource(R.string.retry))
        }
    }
}

Kiểm tra bản xem trước

55cf0c45f5be219f.png

  1. Cập nhật thành phần kết hợp HomeScreen() để truyền hàm thử lại lambda.
@Composable
fun HomeScreen(
   marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
   when (marsUiState) {
       //...

       is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
   }
}
  1. Trong tệp ui/theme/MarsPhotosApp.kt, hãy cập nhật lệnh gọi hàm HomeScreen() để đặt tham số lambda retryAction thành marsViewModel::getMarsPhotos. Thao tác này sẽ truy xuất ảnh sao Hoả từ máy chủ.
HomeScreen(
   marsUiState = marsViewModel.marsUiState,
   retryAction = marsViewModel::getMarsPhotos
)

6. Cập nhật quy trình kiểm thử ViewModel

MarsUiStateMarsViewModel hiện chứa danh sách ảnh thay vì một ảnh duy nhất. Ở trạng thái hiện tại, MarsViewModelTest yêu cầu lớp dữ liệu MarsUiState.Success chứa một thuộc tính chuỗi. Do đó, quy trình kiểm thử không biên dịch. Bạn cần cập nhật quy trình kiểm thử marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() để xác nhận rằng MarsViewModel.marsUiState bằng với trạng thái Success chứa danh sách ảnh.

  1. Mở tệp rules/MarsViewModelTest.kt.
  2. Trong quy trình kiểm thử marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess(), hãy sửa đổi lệnh gọi hàm assertEquals() để so sánh trạng thái Success (truyền danh sách ảnh giả mạo vào thông số ảnh) với marsViewModel.marsUiState.
@Test
    fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
        runTest {
            val marsViewModel = MarsViewModel(
                marsPhotosRepository = FakeNetworkMarsPhotosRepository()
            )
            assertEquals(
                MarsUiState.Success(FakeDataSource.photosList),
                marsViewModel.marsUiState
            )
        }

Giờ đây, quy trình kiểm thử sẽ biên dịch, chạy và thành công!

7. Lấy mã giải pháp

Để tải xuống mã cho lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git này:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip, sau đó giải nén và mở trong Android Studio.

Nếu bạn muốn tham khảo đoạn mã giải pháp cho lớp học lập trình này, hãy xem trên GitHub.

8. Kết luận

Chúc mừng bạn đã hoàn thành lớp học lập trình này và xây dựng ứng dụng Mars Photos! Đã đến lúc khoe với gia đình và bạn bè về ứng dụng này cùng với các hình ảnh chụp cảnh thật trên sao Hoả.

Đừng quên chia sẻ thành quả của bạn lên mạng xã hội với hashtag #AndroidBasics!

9. Tìm hiểu thêm

Tài liệu dành cho nhà phát triển Android:

Tài liệu khác: