Tải và hiện hình ảnh từ Internet

1. Chào mừng mọi người

Giới thiệu

Trong 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 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 trên web. Bạn cũng sẽ ôn lại cách tạo và sử dụng RecyclerView để 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

  • Biết cách tạo và sử dụng các mảnh.
  • Biết cách truy xuất JSON qua dịch vụ web REST và phân tích cú pháp dữ liệu đó thành các đối tượng trong Kotlin bằng cách sử dụng thư viện RetrofitMoshi.
  • Biết cách tạo bố cục lưới bằng RecyclerView.
  • Biết cách hoạt động của Adapter, ViewHolderDiffUtil.

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ừ URL trên web.
  • Cách sử dụng RecyclerView và phương thức tiếp nối bố cục dạng lưới để trình bày hình ảnh theo bố cục dạng 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 MarsPhotos để lấy URL hình ảnh từ dữ liệu về sao Hoả, sử dụng 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.
  • Dùng RecyclerView để hiện các hình ảnh chụp sao Hoả theo bố cục lưới.
  • Thêm trạng thái và cách xử lý lỗi vào RecyclerView.

Bạn cần có

  • Một máy tính sử dụng một 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).
  • Quyền truy cập Internet trên máy tính.

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 MarsPhotos trong lớp học trước. Ứng dụng MarsPhotos kết nối với dịch vụ web để truy xuất và hiển thị số lượng đối tượng Kotlin được truy xuất bằng Retrofit. 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.

Phiên bản ứng dụng mà bạn sẽ tạo trong lớp học lập trình này sẽ đưa vào trang tổng quan các ảnh chụp sao Hoả theo bố cục dạng lưới. Những hình ảnh này nằm trong dữ liệu về sao Hoả 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, cũng như sử dụng RecyclerView để tạo bố cục lưới cho hình ảnh. Ứng dụng của bạn cũng sẽ xử lý lỗi mạng một cách thoả đáng.

243d21747dfb8999.png

3. Hiển thị hình ảnh lấy từ Internet

Việc hiển thị hình ảnh lấy từ URL trên web 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à giải mã từ định dạng nén thành hình ảnh mà Android có thể sử dụng. Hình ảnh phải được lưu vào bộ nhớ đệm tạm thời trên RAM, 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 năng 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 hình ảnh xuống, lưu vào bộ đệm, giải mã và lưu 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 đối tượng ImageView để 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 đơn lẻ 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ề. Sau đây là ảnh chụp màn hình trước và sau khi hiển thị:

Thêm phần phụ thuộc của Coil

  1. Mở ứng dụng giải pháp MarsPhotos từ lớp học lập trình trước.
  2. Chạy ứng dụng để xem cách hoạt động. (Ứng dụng cho biết tổng số ảnh chụp sao Hoả được truy xuất).
  3. Mở build.gradle (Module: app).
  4. Trong phần dependencies, hãy thêm dòng này vào thư viện Coil:
    // Coil
    implementation "io.coil-kt:coil:1.1.1"

Kiểm tra và cập nhật 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. Thư viện Coil được lưu trữ và cung cấp trên kho lưu trữ mavenCentral(). Trong build.gradle (Project: MarsPhotos), hãy thêm mavenCentral() trong khối repositories ở trên cùng.
repositories {
   google()
   mavenCentral()
}
  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.

Cập nhật ViewModel

Ở bước này, bạn sẽ thêm thuộc tính LiveData vào lớp OverviewViewModel để lưu trữ đối tượng Kotlin đã nhận được, đó là MarsPhoto.

  1. Mở overview/OverviewViewModel.kt. Ngay bên dưới phần khai báo thuộc tính _status, hãy thêm thuộc tính có thể thay đổi có tên là _photos thuộc loại MutableLiveData, thuộc tính mới này có thể lưu trữ một đối tượng MarsPhoto duy nhất.
private val _photos = MutableLiveData<MarsPhoto>()

Nhập com.example.android.marsphotos.network.MarsPhoto khi có yêu cầu.

  1. Ngay bên dưới phần khai báo _photos, hãy thêm trường dự phòng công khai có tên là photos thuộc loại LiveData<MarsPhoto>.
val photos: LiveData<MarsPhoto> = _photos
  1. 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.
try {
   val listResult = MarsApi.retrofitService.getPhotos()
   ...
}
  1. Chỉ định ảnh chụp sao Hoả đầu tiên được truy xuất vào biến mới _photos. Thay đổi listResult thành _photos.value. Chỉ định url của hình ảnh đầu tiên tại chỉ mục 0. Thao tác này sẽ gây ra một lỗi, bạn sẽ sửa lỗi đó sau.
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   ...
}
  1. Trong dòng mã tiếp theo, hãy cập nhật status.value thành mã như 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 danh sách hình ảnh.
try {
   ...
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"

}
  1. Khối try{} hoàn chỉnh giờ đây có dạng như sau:
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
  1. Chạy ứng dụng. TextView hiện đang hiển thị URL của ảnh chụp sao Hoả nằm đầu tiên trong danh sách. Đến thời điểm này, bạn đã thiết lập được ViewModelLiveData cho URL đó.

ae99ec8569198456.png

Sử dụng phương thức điều hợp liên kết (Binding Adapter)

Các phương thức điều hợp liên kết (Binding Adapter) là các phương thức có chú giải, dùng để tạo phương thức setter tuỳ chỉnh cho các thuộc tính tuỳ chỉnh trong thành phần hiển thị.

Các phương thức này thường được dùng khi bạn thiết lập một thuộc tính trong XML bằng mã: android:text="Sample Text". Hệ thống Android tự động tìm phương thức setter có cùng tên với thuộc tính text. Thuộc tính này được thiết lập bằng phương thức setText(String: text). Phương thức setText(String: text) là phương thức setter cho một số thành phần hiển thị do Android Framework cung cấp. Bạn có thể tuỳ chỉnh hành vi tương tự bằng cách sử dụng các phương thức điều hợp liên kết (binding adapter); bạn có thể cung cấp thuộc tính tuỳ chỉnh và logic tuỳ chỉnh mà thư viện Liên kết dữ liệu (Data binding) sẽ gọi.

Ví dụ:

Khi bạn thực hiện một thao tác phức tạp hơn là chỉ gọi phương thức setter trên thành phần hiển thị Image, thì thao tác này sẽ thiết lập hình ảnh có thể vẽ. Hãy cân nhắc tải hình ảnh từ Internet bên ngoài luồng giao diện người dùng (luồng chính). Trước tiên, hãy chọn một thuộc tính tuỳ chỉnh để gán hình ảnh cho một ImageView. Trong ví dụ sau, thuộc tính đó là imageUrl.

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:imageUrl="@{product.imageUrl}"/>

Nếu bạn không thêm mã nào khác, hệ thống sẽ tìm phương thức setImageUrl(String) trên ImageView và không tìm thấy phương thức đó. Việc này gây ra lỗi vì đây là thuộc tính tuỳ chỉnh không được cung cấp trong khung lập trình. Bạn phải tạo ra cách để triển khai và thiết lập thuộc tính app:imageUrl thành ImageView. Bạn sẽ dùng các phương thức điều hợp liên kết Binding adapter (tức là các phương thức có chú giải) để thực hiện việc này.

Ví dụ về một phương thức điều hợp liên kết (Binding Adapter):

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        // Load the image in the background using Coil.
        }
    }
}

Chú giải @BindingAdapter nhận tên thuộc tính làm tham số.

Trong phương thức bindImage, tham số phương thức đầu tiên là loại dữ liệu của thành phần hiển thị (View) mục tiêu và thông số thứ hai là giá trị được thiết lập cho thuộc tính.

Trong phương thức này, thư viện Coil sẽ tải hình ảnh bên ngoài luồng giao diện người dùng và đặt hình ảnh này vào ImageView.

Tạo phương thức điều hợp liên kết (binding adapter) và sử dụng Coil

  1. Trong gói com.example.android.marsphotos, hãy tạo một tệp Kotlin tên là BindingAdapters. Tệp này sẽ chứa các phương thức điều hợp liên kết (binding adapter) mà bạn sử dụng trong toàn bộ ứng dụng.

a04afbd6ae8ccfcd.png

  1. Trong BindingAdapters.kt, hãy tạo một hàm bindImage() dưới dạng hàm cấp cao nhất (không nằm trong lớp). Hàm này sẽ nhận ImageViewString làm tham số.
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

Nhập android.widget.ImageView khi có yêu cầu.

  1. Chú giải hàm này bằng @BindingAdapter. Chú giải @BindingAdapter cho biết liên kết dữ liệu sẽ thực thi phương thức điều hợp liên kết (binding adapter) này khi một mục trong thành phần hiển thị (View) có thuộc tính imageUrl.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

Nhập androidx.databinding.BindingAdapter khi có yêu cầu.

Hàm let scope

let là một trong các hàm Scope của Kotlin, cho phép bạn thực thi một khối mã trong ngữ cảnh của một đối tượng. Có 5 hàm Scope trong Kotlin, hãy tham khảo tài liệu này để tìm hiểu thêm.

Cách sử dụng:

let được dùng để gọi một hoặc nhiều hàm trên kết quả của chuỗi lệnh gọi.

Hàm let cùng với toán tử an toàn cho lệnh gọi (?.) được dùng để thực hiện một thao tác an toàn cho giá trị rỗng trên đối tượng. Trong trường hợp này, khối mã let sẽ chỉ được thực thi nếu đối tượng khác rỗng.

  1. Bên trong hàm bindImage(), hãy thêm một khối let{} vào đối số imgUrl bằng cách sử dụng toán tử an toàn cho lệnh gọi.
imgUrl?.let {
}
  1. Trong khối let{}, hãy thêm dòng sau đây để chuyển đổi chuỗi URL thành đối tượng Uri bằng phương thức toUri(). Để sử dụng giao thức HTTPS, hãy thêm buildUpon.scheme("https") vào trình tạo toUri. Hãy gọi build() để tạo đối tượng.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

Nhập androidx.core.net.toUri khi có yêu cầu.

  1. Bên trong khối let{}, sau phần khai báo imgUri, hãy sử dụng load(){} trong Coil để tải hình ảnh từ đối tượng imgUri vào imgView.
imgView.load(imgUri) {
}

Nhập coil.load khi có yêu cầu.

  1. Phương thức hoàn chỉnh của bạn sẽ có dạng như dưới đây:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
        imgView.load(imgUri)
    }
}

Cập nhật bố cục và các mảnh

Trong phần trước, bạn đã sử dụng thư viện hình ảnh Coil để tải hình ảnh. Để xem hình ảnh trên màn hình, bước tiếp theo là cập nhật ImageView bằng thuộc tính mới để hiển thị một hình ảnh duy nhất.

Trong phần sau của lớp học lập trình này, bạn sẽ dùng res/layout/grid_view_item.xml làm tệp tài nguyên bố cục cho mỗi mục của bố cục lưới trong RecyclerView. Trong nhiệm vụ này, bạn sẽ tạm thời sử dụng tệp này để hiển thị hình ảnh bằng URL hình ảnh mà bạn đã truy xuất trong nhiệm vụ trước đó. Tạm thời, bạn sẽ sử dụng tệp bố cục này thay cho fragment_overview.xml.

  1. Mở res/layout/grid_view_item.xml.
  2. Phía trên phần tử <ImageView>, hãy thêm một phần tử <data> cho liên kết dữ liệu và liên kết với lớp OverviewViewModel:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
  1. Hãy thêm thuộc tính app:imageUrl vào phần tử ImageView để sử dụng phương thức điều hợp liên kết (binding adapter) cho việc tải hình ảnh mới. Hãy nhớ rằng photos chứa danh sách MarsPhotos được truy xuất từ máy chủ. Gán URL đầu tiên cho thuộc tính imageUrl.
    <ImageView
        android:id="@+id/mars_image"
        ...
        app:imageUrl="@{viewModel.photos.imgSrcUrl}"
        ... />
  1. Mở overview/OverviewFragment.kt. Trong phương thức onCreateView(), hãy đánh dấu ghi chú vào dòng mã làm tăng cường lớp FragmentOverviewBinding rồi gán nó cho biến liên kết. Bạn sẽ thấy lỗi do xoá dòng này. Lỗi này chỉ là tạm thời; bạn sẽ khắc phục vấn đề sau.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Hãy sử dụng grid_view_item.xml thay vì fragment_overview.xml.. Thêm dòng sau để tăng cường lớp GridViewItemBinding.
val binding = GridViewItemBinding.inflate(inflater)

Nhập com.example.android.marsphotos. databinding.GridViewItemBinding khi có yêu cầu.

  1. Chạy ứng dụng. Bây giờ, bạn sẽ thấy một ảnh chụp sao Hoả.

e59b6e849e63ae2b.png

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

Khi sử dụng Coil, bạn có thể cải thiện trải nghiệm người dùng bằng cách hiển thị biểu tượng giữ chỗ trong khi tải hình ảnh và hiển thị biểu tượng lỗi nếu không tải được, chẳng hạn như khi không tìm thấy hình ảnh hoặc hình ảnh bị hỏng. Ở bước này, bạn sẽ thêm chức năng đó vào phương thức điều hợp liên kết.

  1. Mở res/drawable/ic_broken_image.xml và nhấp vào thẻ Design (Thiết kế) ở bên phải. Đối với hình ảnh thể hiện lỗi, bạn đang sử 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ơ có thể vẽ này sử dụng thuộc tính android:tint để làm biểu tượng có màu xám.

467c213c859e1904.png

  1. Mở res/drawable/loading_animation.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 hiệu ứng động trong bản xem trước).

6c1f87d1c932c762.png

  1. Quay lại tệp BindingAdapters.kt. Trong phương thức bindImage(), hãy cập nhật lệnh gọi thành imgView.load(imgUri) để thêm một hàm lambda như sau: Mã này thiết lập hình ảnh giữ chỗ có biểu tượng đang tải để sử dụng trong khi tải (thành phần vẽ được loading_animation). Mã này cũng thiết lập một hình ảnh để sử dụng nếu hình ảnh không tải được (thành phần vẽ được broken_image).
imgView.load(imgUri) {
   placeholder(R.drawable.loading_animation)
   error(R.drawable.ic_broken_image)
}
  1. Phương thức bindImage() hoàn chỉnh lúc này sẽ có dạng như sau:
@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. Chạy ứng dụng. Tuỳ thuộc vào tốc độ kết nối mạng, bạn có thể sẽ thấy biểu tượng đang tải trong giây lát khi Coil hình ảnh thuộc tính xuống tải và hiển thị. 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.

6dcecd205a0741a.gif

  1. Huỷ bỏ các thay đổi tạm thời mà bạn đã thực hiện trong overview/OverviewFragment.kt. Trong phương thức onCreateview(), hãy bỏ đánh dấu ghi chú trên dòng mã làm tăng cường FragmentOverviewBinding. Xoá hoặc đánh dấu ghi chú cho dòng làm tăng cường GridViewIteMBinding.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)

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

Ứng dụng của bạn hiện đang tải ảnh chụp sao Hoả từ Internet. Bằng cách sử dụng dữ liệu từ mục MarsPhoto đầu tiên trong danh sách, bạn đã tạo một thuộc tính LiveData trong ViewModel và 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 ImageView. Tuy nhiên, mục tiêu của bạn là hiển thị lưới hình ảnh cho ứng dụng, do đó, trong nhiệm vụ này, bạn sẽ sử dụng RecyclerView với trình quản lý bố cục Lưới (Grid) để hiển thị lưới hình ảnh.

Cập nhật view model

Ở nhiệm vụ trước, trong OverviewViewModel, bạn đã thêm đối tượng LiveData có tên là _photos. Đối tượng này chứa một đối tượng MarsPhoto – mục đầu tiên trong danh sách phản hồi từ dịch vụ web. Trong bước này, bạn sẽ thay đổi LiveData này để lưu toàn bộ danh sách đối tượng MarsPhoto.

  1. Mở overview/OverviewViewModel.kt.
  2. Thay đổi loại _photos thành danh sách đối tượng MarsPhoto.
private val _photos = MutableLiveData<List<MarsPhoto>>()
  1. Thay loại photos của thuộc tính dự phòng thành loại List<MarsPhoto>:
 val photos: LiveData<List<MarsPhoto>> = _photos
  1. Di chuyển xuống khối try {} bên trong phương thức getMarsPhotos(). MarsApi.retrofitService.getPhotos()

trả về danh sách đối tượng MarsPhoto, bạn chỉ cần gán cho _photos.value.

_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
  1. Khối try/catch hoàn chỉnh giờ đây có dạng như sau:
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
    _status.value = "Failure: ${e.message}"
}

Bố cục lưới

GridLayoutManager cho RecyclerView bố trí dữ liệu dưới dạng lưới có thể cuộn, như minh hoạ dưới đây.

fcf0fc4b78f8650.png

Từ góc độ thiết kế, Bố cục lưới (Grid Layout) là phù hợp nhất đối với các danh sách có thể được biểu thị dưới dạng biểu tượng hoặc hình ảnh, chẳng hạn như danh sách trong ứng dụng duyệt xem ảnh Mars của bạn.

Cách bố cục Lưới trình bày các mục

Bố cục lưới sắp xếp các mục thành một lưới gồm các hàng và cột. Với chức năng cuộn theo chiều dọc (theo mặc định), mỗi mục trong một hàng sẽ chiếm một "span". Một mục có thể chiếm nhiều span. Trong trường hợp bên dưới, một span tương đương với chiều rộng của một cột là 3.

Trong hai ví dụ minh hoạ dưới đây, mỗi hàng được tạo thành từ ba span. Theo mặc định, GridLayoutManager bố trí mỗi mục trong một span cho đến khi hết số span mà bạn chỉ định. Khi đạt đến số lượng span chỉ định, các mục sẽ chuyển xuống dòng tiếp theo.

Thêm Recyclerview

Trong bước này, bạn sẽ thay đổi bố cục của ứng dụng để sử dụng thành phần hiển thị recycler view theo bố cục lưới, thay vì thành phần hiển thị hình ảnh đơn.

  1. Mở layout/grid_view_item.xml. Xoá biến dữ liệu viewModel.
  2. Bên trong thẻ <data>, hãy thêm biến photo thuộc loại MarsPhoto như sau.
<data>
   <variable
       name="photo"
       type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
  1. Trong <ImageView>, hãy thay đổi thuộc tính app:imageUrl để tham chiếu đến URL hình ảnh trong đối tượng MarsPhoto. Những thay đổi này sẽ huỷ các thay đổi tạm thời mà bạn đã thực hiện trong nhiệm vụ trước.
app:imageUrl="@{photo.imgSrcUrl}"
  1. Mở layout/fragment_overview.xml. Xoá toàn bộ phần tử <TextView>.
  2. Thêm phần tử <RecyclerView> sau. Thiết lập giá trị nhận dạng thành photos_grid, thiết lập các thuộc tính widthheight thành 0dp để phần tử này có thể lấp đầy ConstraintLayout gốc. Bạn sẽ sử dụng bố cục Lưới, do đó, hãy thiết lập thuộc tính layoutManager thành androidx.recyclerview.widget.GridLayoutManager. Thiết lập spanCount thành 2 để bạn có hai cột.
<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. Để xem trước mã trên trong chế độ xem Design (Thiết kế), hãy sử dụng tools:itemCount để thiết lập 16 làm số lượng mục xuất hiện trong bố cục. Thuộc tính itemCount chỉ định số lượng mục mà trình chỉnh sửa bố cục sẽ hiển thị trong cửa sổ Preview (Xem trước). Sử dụng tools:listitem để thiết lập grid_view_item thành bố cục của các mục trong danh sách.
<androidx.recyclerview.widget.RecyclerView
            ...
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />
  1. Chuyển sang chế độ xem Design (Thiết kế), bạn sẽ thấy bản xem trước tương tự như ảnh chụp màn hình bên dưới. Đây không phải ảnh chụp sao Hoả, nhưng vẫn giúp bạn hình dung được bố cục lưới recyclerview sẽ trông như thế nào. Bản xem trước sử dụng khoảng đệm và bố cục grid_view_item cho mọi mục trong lưới trong recyclerview.

20742824367c3952.png

  1. Theo Nguyên tắc thiết kế Material Design, bạn nên có khoảng trống 8dp ở lề đầu, lề cuối và các cạnh của danh sách, cũng như khoảng trống 4dp giữa các mục. Bạn có thể thực hiện việc này bằng cách kết hợp khoảng đệm trong bố cục fragment_overview.xml và bố cục grid_view_item.xml.

a3561fa85fea7a8f.png

  1. Mở layout/gridview_item.xml. Hãy để ý thuộc tính padding, bạn đã có khoảng đệm 2dp giữa viền bên ngoài của mục và nội dung bên trong mục. Điều này sẽ giúp chúng ta có khoảng trống 4dp giữa nội dung của các mục và 2dp dọc các cạnh bên ngoài. Như vậy có nghĩa là chúng ta sẽ cần thêm khoảng đệm 6dp cho các cạnh bên ngoài để đúng với nguyên tắc thiết kế.
  2. Quay lại layout/fragment_overview.xml. Thêm khoảng đệm 6dp cho RecyclerView. Từ đây, bạn sẽ có 8dp ở bên ngoài và 4dp ở bên trong như theo nguyên tắc.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:padding="6dp"
            ...  />
  1. Phần tử <RecyclerView> hoàn chỉnh sẽ có dạng như sau.
<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"  />

Thêm phương thức tiếp nối lưới ảnh

Bố cục fragment_overview hiện đang có RecyclerView theo bố cục lưới. Trong bước này, bạn sẽ liên kết dữ liệu đã truy xuất từ máy chủ web với RecyclerView thông qua phương thức tiếp nối RecyclerView.

ListAdapter (Trình làm mới)

ListAdapter là một lớp con của lớp RecyclerView.Adapter để trình bày dữ liệu Danh sách theo RecyclerView, trong đó có cả dữ liệu so sánh khác biệt về lập trình giữa các Danh sách trên luồng chạy nền.

Trong ứng dụng này, bạn sẽ triển khai DiffUtil trong ListAdapter. Lợi thế của việc sử dụng DiffUtil là mỗi khi thêm, xoá hoặc thay đổi một mục nào đó trong RecyclerView, thì toàn bộ danh sách cũng không được làm mới. Chỉ các mục đã thay đổi mới được làm mới.

Thêm ListAdapter vào ứng dụng.

  1. Trong gói overview, hãy tạo một lớp Kotlin có tên là PhotoGridAdapter.
  2. Mở rộng lớp PhotoGridAdapter từ ListAdapter với các tham số hàm khởi tạo như bên dưới. Lớp PhotoGridAdapter mở rộng ListAdapter, trong đó hàm khởi tạo cần có loại của mục trong danh sách, thành phần chứa chế độ xem và phương thức triển khai DiffUtil.ItemCallback.
class PhotoGridAdapter : ListAdapter<MarsPhoto,
        PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}

Nhập các lớp androidx.recyclerview.widget.ListAdaptercom.example.android.marsphoto.network.MarsPhoto nếu có yêu cầu. Trong các bước sau, bạn sẽ triển khai các phương thức triển khai còn thiếu khác của hàm khởi tạo này, đây cũng là nguyên nhân đang gây ra lỗi.

  1. Để giải quyết các lỗi ở trên, bạn sẽ thêm các phương thức cần có trong bước này và triển khai các phương thức đó trong phần sau của nhiệm vụ này. Nhấp vào lớp PhotoGridAdapter, nhấp vào biểu tượng bóng đèn màu đỏ, từ trình đơn thả xuống, hãy chọn Implement members (Triển khai thành phần). Trong cửa sổ đang bật lên, hãy chọn các phương thức ListAdapter, onCreateViewHolder()onBindViewHolder(). Android Studio sẽ vẫn hiện các lỗi mà bạn sẽ khắc phục trong phần cuối của nhiệm vụ này.
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")
}

Để triển khai các phương thức onCreateViewHolderonBindViewHolder, bạn cần có MarsPhotoViewHolder để thêm trong bước tiếp theo.

  1. Bên trong PhotoGridAdapter, hãy thêm một khai báo lớp chồng nhau cho MarsPhotoViewHolder, giúp mở rộng RecyclerView.ViewHolder. Bạn cần biến GridViewItemBinding để liên kết MarsPhoto với bố cục, vì vậy hãy truyền biến này vào MarsPhotoViewHolder. Lớp ViewHolder cơ sở cần phải có một thành phần hiển thị trong hàm khởi tạo, hãy truyền thành phần hiển thị gốc được liên kết vào lớp này.
class MarsPhotoViewHolder(private var binding:
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {
}

Nhập androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding nếu có yêu cầu.

  1. Trong MarsPhotoViewHolder, hãy tạo một phương thức bind(). Phương thức này nhận đối tượng MarsPhoto làm đối số và thiết lập binding.property thành đối tượng đó. Gọi executePendingBindings() sau khi thiết lập thuộc tính. Thao tác này sẽ khiến mã mới thực thi ngay lập tức.
fun bind(MarsPhoto: MarsPhoto) {
   binding.photo = MarsPhoto
   binding.executePendingBindings()
}
  1. Vẫn bên trong lớp PhotoGridAdapter trong onCreateViewHolder(), hãy xoá TODO và thêm dòng bên dưới. Phương thức onCreateViewHolder() cần trả về một MarsPhotoViewHolder mới được tạo bằng cách tăng cường GridViewItemBinding và sử dụng LayoutInflater từ ngữ cảnh ViewGroup gốc.
   return MarsPhotoViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))

Nhập android.view.LayoutInflater khi có yêu cầu.

  1. Trong phương thức onBindViewHolder(), hãy xoá TODO và thêm các dòng bên dưới. Ở đây, bạn gọi getItem() để lấy đối tượng MarsPhoto được liên kết với vị trí RecyclerView hiện tại, sau đó truyền thuộc tính đó vào phương thức bind() trong MarsPhotoViewHolder.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
  1. Bên trong PhotoGridAdapter, hãy thêm khai báo đối tượng đồng hành cho DiffCallback, như minh hoạ bên dưới.
    Đối tượng DiffCallback mở rộng DiffUtil.ItemCallback có loại đối tượng chung mà bạn muốn so sánh — MarsPhoto. Bạn sẽ so sánh hai đối tượng ảnh sao Hoả trong phương thức triển khai này.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}

Nhập androidx.recyclerview.widget.DiffUtil khi có yêu cầu.

  1. Nhấn vào biểu tượng bóng đèn màu đỏ để triển khai các phương thức so sánh cho đối tượng DiffCallback, đó là phương thức 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. Trong phương thức areItemsTheSame(), hãy xoá TODO. Phương thức này được DiffUtil gọi để quyết định xem hai đối tượng này có đại diện cho cùng một Mục (Item) hay không. DiffUtil sử dụng phương thức này để xác định xem đối tượng MarsPhoto mới có giống với đối tượng MarsPhoto cũ hay không. Giá trị nhận dạng của mỗi mục (đối tượng MarsPhoto) đều là duy nhất. So sánh giá trị nhận dạng của oldItemnewItem, sau đó trả về kết quả.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.id == newItem.id
}
  1. Trong areContentsTheSame(), hãy xoá TODO. Phương thức này được DiffUtil gọi khi cần kiểm tra xem hai mục có cùng một dữ liệu hay không. Dữ liệu quan trọng trong MarsPhoto là URL hình ảnh. So sánh URL của oldItemnewItem, sau đó trả về kết quả.
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.imgSrcUrl == newItem.imgSrcUrl
}

Hãy đảm bảo rằng bạn có thể biên dịch và chạy ứng dụng mà không xảy ra lỗi nào, nhưng trình mô phỏng sẽ hiện màn hình trống. Bạn đã có recyclerview nhưng chưa có dữ liệu nào truyền vào đó. Bạn sẽ triển khai việc truyền dữ liệu trong bước tiếp theo.

Thêm phương thức điều hợp liên kết (binding adapter) và kết nối các thành phần với nhau

Ở bước này, bạn sẽ dùng BindingAdapter để khởi tạo PhotoGridAdapter với danh sách các đối tượng MarsPhoto. Việc sử dụng BindingAdapter để thiết lập dữ liệu RecyclerView sẽ khiến dữ liệu liên kết tự động theo dõi LiveData cho danh sách đối tượng MarsPhoto. Sau đó, phương thức điều hợp liên kết (binding adapter) được gọi tự động khi danh sách MarsPhoto thay đổi.

  1. Mở BindingAdapters.kt.
  2. Ở cuối tệp, hãy thêm phương thức bindRecyclerView() nhận RecyclerView và danh sách đối tượng MarsPhoto làm đối số. Chú giải phương thức đó bằng @BindingAdapter có thuộc tính listData.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
    data: List<MarsPhoto>?) {
}

Nhập androidx.recyclerview.widget.RecyclerViewcom.example.android.marsphotos.network.MarsPhoto nếu có yêu cầu.

  1. Bên trong hàm bindRecyclerView(), hãy truyền recyclerView.adapter cho PhotoGridAdapter và chỉ định thuộc tính này cho một thuộc tính val mới adapter.
val adapter = recyclerView.adapter as PhotoGridAdapter
  1. Ở cuối hàm bindRecyclerView(), hãy gọi adapter.submitList() bằng dữ liệu về danh sách ảnh sao Hoả. Thao tác này sẽ cho RecyclerView biết khi có danh sách mới.
adapter.submitList(data)

Nhập com.example.android.marsrealestate.overview.PhotoGridAdapter khi có yêu cầu.

  1. Phương thức điều hợp liên kết bindRecyclerView hoàn chỉnh sẽ có dạng như sau:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
                    data: List<MarsPhoto>?) {
   val adapter = recyclerView.adapter as PhotoGridAdapter
   adapter.submitList(data)

}
  1. Để kết nối mọi thứ với nhau, hãy mở res/layout/fragment_overview.xml. Thêm thuộc tính app:listData vào phần tử RecyclerView và thiết lập thành viewmodel.photos bằng liên kết dữ liệu. Việc này tương tự như những gì bạn đã làm cho ImageView trong một nhiệm vụ trước đó.
app:listData="@{viewModel.photos}"
  1. Mở overview/OverviewFragment.kt. Trong onCreateView(), ngay trước câu lệnh return, hãy khởi tạo phương thức tiếp nối RecyclerView trong binding.photosGrid thành một đối tượng PhotoGridAdapter mới.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. Chạy ứng dụng. Bạn sẽ thấy một lưới cuộn các hình ảnh về sao Hoả. Khi bạn cuộn để xem hình ảnh mới thì có cảm giác hơi lạ. Ở lề trên và lề dưới của RecyclerView vẫn có khoảng đệm khi bạn cuộn, khiến danh sách trông như đang không cuộn dưới Thanh thao tác.

5d03641aa1589842.png

  1. Để khắc phục, bạn cần dùng thuộc tính android:clipToPadding để yêu cầu RecyclerView không cắt nội dung bên trong thành khoảng đệm. Thao tác này sẽ tạo ra chế độ xem dạng cuộn trong khoảng đệm. Quay lại layout/fragment_overview.xml. Thêm thuộc tính android:clipToPadding cho RecyclerView, thiết lập thành false.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:clipToPadding="false"
            ...  />
  1. Chạy ứng dụng. Hãy lưu ý rằng ứng dụng cũng hiện biểu tượng đang tải trước khi hiện chính hình ảnh đó, đúng như dự kiến. Đây là hình ảnh giữ chỗ trong quá trình tải mà bạn đã truyền vào thư viện hình ảnh Coil.

3128b84aa22ef97e.png

  1. Khi ứng dụng đang chạy, hãy bật chế độ trên máy bay. 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 đã chuyể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.

28d2cbba564f35ff.png

Xin chúc mừng, bạn sắp hoàn thành rồi. Trong nhiệm vụ tiếp theo, bạn sẽ cải thiện trải nghiệm người dùng bằng cách bổ sung khả năng xử lý lỗi vào ứng dụng.

5. Thêm tính năng xử lý lỗi trong RecyclerView

Ứng dụng MarsPhotos hiện biểu tượng hình ảnh bị hỏng khi không thể tìm nạp hình ảnh. Tuy nhiên, khi không có mạng, ứng dụng sẽ hiện một màn hình trống. Bạn sẽ xác minh màn hình trống trong bước tiếp theo.

  1. Bật chế độ trên máy bay trên thiết bị hoặc trình mô phỏng của bạn. Chạy ứng dụng trong Android Studio. Bạn sẽ thấy màn hình trống.

492011786c2dd7f7.png

Đây không phải là một trải nghiệm tốt cho người dùng. Trong nhiệm vụ này, bạn sẽ thêm cách xử lý lỗi cơ bản để giúp người dùng hiểu rõ hơn về sự cố đang xảy ra. Nếu không có Internet, ứng dụng sẽ hiện biểu tượng lỗi kết nối và trong lúc ứng dụng đang tìm nạp danh sách MarsPhoto, ứng dụng sẽ hiện ảnh động biểu thị trạng thái đang tải.

Thêm trạng thái vào ViewModel

Trong nhiệm vụ này, bạn sẽ tạo một thuộc tính trong OverviewViewModel để biểu thị trạng thái của yêu cầu web. Có 3 trạng thái cần quan tâm: đang tải, thành công và không thành công. Trạng thái đang tải này là khi bạn chờ truy xuất dữ liệu. Trạng thái thành công là khi chúng tôi truy xuất dữ liệu thành công từ dịch vụ web. Trạng thái lỗi cho biết lỗi mạng hoặc lỗi kết nối.

Lớp enum trong Kotlin

Để thể hiện 3 trạng thái này trong ứng dụng, bạn sẽ sử dụng enum. enum là viết tắt của enumeration, có nghĩa là hành động liệt kê tất cả các mục có đánh dấu thứ tự trong một tập hợp. Mỗi hằng enum là một đối tượng của lớp enum.

Trong Kotlin, enum là loại dữ liệu có thể chứa một tập hợp các hằng. Bạn khai báo các hằng này bằng cách thêm từ khoá enum vào trước một khai báo lớp như bên dưới. Các hằng enum được phân tách với nhau bằng dấu phẩy.

Khai báo:

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

Cách sử dụng:

var direction : Direction = Direction.NORTH

Như đã trình bày ở trên, các đối tượng enum có thể được tham chiếu bằng tên lớp đứng trước toán tử dấu chấm (.) và tên của hằng.

Thêm khai báo lớp có các giá trị trạng thái trong Viewmodel.

  1. Mở overview/OverviewViewModel.kt. Ở đầu tệp (sau phần nội dung nhập, trước phần khai báo lớp), hãy thêm enum để biểu thị tất cả trạng thái đang có:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. Di chuyển đến phần khai báo của thuộc tính _statusstatus, thay đổi loại dữ liệu từ String thành MarsApiStatus. MarsApiStatus. Đây là lớp enum mà bạn đã khai báo ở bước trước.
private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus> = _status
  1. Trong phương thức getMarsPhotos(), hãy thay đổi chuỗi "Success: ..." thành trạng thái MarsApiStatus.DONE và chuỗi "Failure..." thành MarsApiStatus.ERROR.
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = MarsApiStatus.DONE
} catch (e: Exception) {
     _status.value = MarsApiStatus.ERROR
}
  1. Thiết lập trạng thái thành MarsApiStatus.LOADING phía trên khối try {}. Đây là trạng thái ban đầu khi coroutine đang chạy và bạn đang chờ dữ liệu. Khối viewModelScope.launch {} hoàn chỉnh giờ đây có dạng như sau:
viewModelScope.launch {
            _status.value = MarsApiStatus.LOADING
            try {
                _photos.value = MarsApi.retrofitService.getPhotos()
                _status.value = MarsApiStatus.DONE
            } catch (e: Exception) {
                _status.value = MarsApiStatus.ERROR
            }
        }
  1. Sau trạng thái lỗi trong khối catch {}, hãy thiết lập _photos thành một danh sách trống. Thao tác này sẽ xoá thành phần hiển thị tái chế (Recycler view).
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _photos.value = listOf()
}
  1. Phương thức getMarsPhotos() hoàn chỉnh sẽ có dạng như sau:
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()
        }
    }
}

Bạn đã khai báo các trạng thái enum cho phần trạng thái, thiết lập trạng thái đang tải ở đầu coroutine, đặt giá trị "done" ("xong") khi ứng dụng của bạn truy xuất xong dữ liệu từ máy chủ web và thiết lập "error" ("lỗi") khi có ngoại lệ. Trong nhiệm vụ tiếp theo, bạn sẽ dùng phương thức điều hợp liên kết (binding adapter) để hiện các biểu tượng tương ứng.

Thêm phương thức điều hợp liên kết cho trạng thái ImageView

Bạn đã thiết lập MarsApiStatus trong OverviewViewModel bằng cách sử dụng một tập hợp các trạng thái enum. Ở bước này, bạn sẽ làm cho trạng thái đó xuất hiện trong ứng dụng. Bạn sử dụng phương thức điều hợp liên kết (binding adapter) cho ImageView để hiện biểu tượng cho trạng thái đang tải và trạng thái lỗi. Khi ứng dụng ở trạng thái đang tải hoặc trạng thái lỗi, ImageView phải xuất hiện. Khi ứng dụng tải xong, ImageView sẽ không xuất hiện.

  1. Mở BindingAdapters.kt, di chuyển đến cuối tệp để thêm phương thức tiếp nối khác. Thêm một phương thức điều hợp liên kết mới có tên là bindStatus(). Các phương thức tiếp nối này sẽ nhận ImageView và giá trị MarsApiStatus làm đối số. Chú giải phương thức này bằng @BindingAdapter, trong đó truyền vào thuộc tính tuỳ chỉnh marsApiStatus làm tham số.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
          status: MarsApiStatus?) {
}

Nhập com.example.android.marsrealestate.overview.MarsApiStatus khi có yêu cầu.

  1. Thêm một khối when {} bên trong phương thức bindStatus() để chuyển đổi giữa các trạng thái.
when (status) {

}
  1. Bên trong when {}, hãy thêm một trường hợp cho trạng thái đang tải (MarsApiStatus.LOADING). Đối với trạng thái này, hãy thiết lập ImageView thành visible (hiển thị) rồi gán cho ảnh động biểu thị trạng thái đang tải. Đây cũng là ảnh động có thể vẽ mà bạn đã sử dụng cho Coil trong nhiệm vụ trước.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}

Nhập android.view.View khi có yêu cầu.

  1. Thêm một trường hợp cho trạng thái lỗi: MarsApiStatus.ERROR. Tương tự như những gì bạn đã làm cho trạng thái LOADING, hãy thiết lập trạng thái ImageView thành "hiển thị" và sử dụng đối tượng có thể vẽ biểu thị lỗi kết nối.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. Thêm một trường hợp cho trạng thái done (hoàn tất): MarsApiStatus.DONE. Ở đây, bạn nhận được phản hồi thành công, vì vậy, hãy thiết lập chế độ hiển thị của trạng thái ImageView thành View.GONE để ẩn trạng thái đó.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Bạn đã thiết lập phương thức điều hợp liên kết cho chế độ xem Hình ảnh. Trong bước tiếp theo, bạn sẽ thêm chế độ xem Hình ảnh sử dụng phương thức điều hợp liên kết mới.

Thêm trạng thái ImageView

Trong bước này, bạn sẽ thêm thành phần hiển thị Hình ảnh trong fragment_overview.xml để hiện trạng thái mà bạn đã khai báo trước đó.

  1. Mở res/layout/fragment_overview.xml. Bên trong ConstraintLayout, bên dưới phần tử RecyclerView, hãy thêm ImageView như dưới đây.
<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 ở trên có các quy tắc giới hạn tương tự như RecyclerView. Tuy nhiên, chiều rộng và chiều cao sử dụng wrap_content để căn giữa hình ảnh thay vì kéo giãn hình ảnh để lấp đầy thành phần hiển thị. Ngoài ra, hãy lưu ý thuộc tính app:marsApiStatus được đặt thành viewModel.status. Thuộc tính này sẽ gọi BindingAdapter của bạn khi thuộc tính trạng thái trong ViewModel thay đổi.

  1. Để kiểm tra mã ở trên, hãy 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ị của bạn. Khi biên dịch và chạy ứng dụng, bạn sẽ nhận thấy hình ảnh lỗi xuất hiện:

a91ddb1c89f2efec.png

  1. Nhấn vào nút Quay lại để đóng ứng dụng và tắt chế độ trên máy bay. Sử dụng màn hình các ứng dụng gần đây để trở lại ứng dụng. Tuỳ thuộc vào tốc độ kết nối mạng, bạn có thể thấy vòng quay đang tải chỉ trong giây lát khi ứng dụng truy vấn dịch vụ web trước khi hình ảnh bắt đầu tải.

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 MarsPhotos! Đã đế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ả.

6. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong dự án bên dưới. Sử dụng nhánh main để lấy hoặc tải mã này xuống.

Để nhận mã cho lớp học lập trình này và mở mã đó trong Android Studio, hãy làm theo các bước sau.

Lấy mã

  1. Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
  2. Trên trang GitHub của dự án, hãy nhấp vào nút Code (Mã), một hộp thoại sẽ xuất hiện.

5b0a76c50478a73f.png

  1. Trong hộp thoại này, hãy nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).

36cc44fcf0f89a1d.png

Lưu ý: Nếu Android Studio đã mở sẵn thì thay vào đó, hãy chọn tuỳ chọn sau đây trong trình đơn File > New > Import Project (Tệp > Mới > Nhập dự án).

21f3eec988dcfbe9.png

  1. Trong hộp thoại Import Project (Nhập dự án), hãy chuyển đến nơi chứa thư mục dự án đã giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 11c34fc5e516fb1c.png để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
  5. Duyệt qua các tệp dự án trong cửa sổ công cụ Project (Dự án) để xem cách ứng dụng được thiết lập.

7. Tóm tắt

  • Thư viện Coil giúp đơn giản hoá quá trình quản lý hình ảnh, chẳng hạn như tải hình ảnh xuống, lưu vào bộ đệm, giải mã và lưu vào bộ nhớ đệm trong ứng dụng.
  • Các phương thức điều hợp liên kết (binding adapter) là các phương thức mở rộng giúp kết nối giữa một thành phần hiển thị và dữ liệu liên kết của thành phần hiển thị đó. Các phương thức điều hợp liên kết (binding adapter) cung cấp hành vi tuỳ chỉnh khi dữ liệu thay đổi, chẳng hạn như gọi Coil để tải hình ảnh từ một URL vào ImageView.
  • Các phương thức điều hợp liên kết (binding adapter) là các phương thức mở rộng có chú giải @BindingAdapter.
  • Để hiện các hình ảnh theo bố cục lưới, hãy sử dụng RecyclerViewGridLayoutManager.
  • Để cập nhật danh sách thuộc tính khi danh sách này thay đổi, hãy sử dụng phương thức điều hợp liên kết (binding adapter) giữa RecyclerView và bố cục.

8. Tìm hiểu thêm

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

Các tài liệu khác: