Kiến thức cơ bản về Kotlin cho Android 08.2: Tải và hiển thị hình ảnh trên Internet

Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.

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 dữ liệu. 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 RecyclerView để trình bày hình ảnh theo bố cục lưới trên trang tổng quan.

Kiến thức bạn cần có

  • Cách tạo và sử dụng các mảnh.
  • Cách sử dụng các thành phần cấu trúc, bao gồm cả mô hình hiển thị, nhà máy mô hình hiển thị, các phép biến đổi và LiveData.
  • 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 Glide để tải và hiển thị hình ảnh lấy từ một URL cho 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.

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng MarsRealEstate để lấy URL hình ảnh từ dữ liệu về bất động sản trên sao Hoả, sau đó sử dụng Glide để 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 về tài sản trên sao Hoả theo bố cục lưới.
  • Thêm trạng thái và cách xử lý lỗi vào RecyclerView.

Trong lớp học lập trình này (và các lớp học lập trình liên quan), bạn sẽ làm việc với một ứng dụng có tên là MarsRealEstate, ứng dụng này đăng tin rao bán bất động sản trên sao Hoả. Ứng dụng này kết nối với một máy chủ Internet để truy xuất và hiển thị dữ liệu về tài sản, bao gồm cả các thông tin chi tiết như giá và việc tài sản có đang được bán hoặc cho thuê hay không. Những hình ảnh đại diện cho từng đối tượng là ảnh chụp sao Hoả thực tế được chụp qua thiết bị thám hiểm sao Hoả của NASA.

Phiên bản ứng dụng mà bạn 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 theo bố cục dạng lưới. Những hình ảnh này nằm trong dữ liệu về tài sản mà ứng dụng của bạn nhận được từ dịch vụ web về bất động sản trên sao Hoả. Ứng dụng của bạn sẽ sử dụng thư viện Glide để 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.

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 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. 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. Bản thân việc tìm hiểu cách tải hình ảnh hiệu quả từ mạng có thể là một lớp học lập trình.

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à Glide để tải xuống, lưu vào bộ đệm, giải mã và lưu hình ảnh vào bộ nhớ đệm. Glide giúp bạn giảm bớt rất nhiều công sức so với việc bạn phải làm tất cả những việc này từ đầu.

Về cơ bản, Glide 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 Glide để hiển thị một hình ảnh từ dịch vụ web về bất động sản. Bạn cho hiện hình ảnh đại diện cho tài sản đầu tiên trên sao Hoả trong danh sách tài sản 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ị:

Bước 1: Thêm phần phụ thuộc Glide

  1. Mở ứng dụng MarsRealEstate từ lớp học lập trình gần đây nhất. (Bạn có thể tải MarsRealEstateNetwork xuống tại đây nếu chưa có ứng dụng này.)
  2. Chạy ứng dụng để xem cách hoạt động. (Nội dung này hiển thị thông tin chi tiết bằng văn bản về một tài sản giả định có trên sao Hoả.)
  3. Mở build.gradle (Module: app).
  4. Trong phần dependencies, hãy thêm dòng này để gọi thư viện Glide:
implementation "com.github.bumptech.glide:glide:$version_glide"


Xin lưu ý rằng số phiên bản đã được xác định riêng trong tệp Gradle của dự án.

  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.

Bước 2: Cập nhật view model

Tiếp theo, bạn sẽ cập nhật lớp OverviewViewModel để thêm dữ liệu trực tiếp cho một cơ sở lưu trú duy nhất trên sao Hoả.

  1. Mở overview/OverviewViewModel.kt. Ngay bên dưới LiveData cho _response, hãy thêm cả dữ liệu trực tiếp nội bộ (có thể thay đổi) và bên ngoài (không thể thay đổi) cho một đối tượng MarsProperty duy nhất.

    Nhập lớp MarsProperty (com.example.android.marsrealestate.network.MarsProperty) khi có yêu cầu.
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. Trong phương thức getMarsRealEstateProperties(), hãy tìm dòng mã bên trong khối try/catch {} có vai trò thiết lập _response.value thành số lượng thuộc tính. Thêm thử nghiệm như minh hoạ bên dưới. Nếu có các đối tượng MarsProperty, thì bài kiểm thử này sẽ đặt giá trị của _property LiveData thành thuộc tính đầu tiên trong listResult.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

Khối try/catch {} hoàn chỉnh giờ đây có dạng như sau:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. Mở tệp res/layout/fragment_overview.xml. Trong phần tử <TextView>, hãy thay đổi android:text để liên kết với thành phần imgSrcUrl của property LiveData:
android:text="@{viewModel.property.imgSrcUrl}"
  1. Chạy ứng dụng. TextView chỉ hiển thị URL của hình ảnh trong thuộc tính đầu tiên của sao Hoả. Đến thời điểm này, bạn đã thiết lập được mô hình hiển thị và dữ liệu trực tiếp cho URL đó.

Bước 3: Tạo phương thức điều hợp liên kết và gọi Glide

Giờ đây, bạn đã có URL của một hình ảnh để hiển thị và đã đến lúc bắt đầu làm việc với Glide để tải hình ảnh đó. Ở bước này, bạn sử dụng một phương thức điều hợp liên kết để lấy URL từ một thuộc tính XML được liên kết với một ImageView, đồng thời sử dụng Glide để tải hình ảnh. Các phương thức điều hợp liên kết 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 để cung cấp hành vi tuỳ chỉnh khi dữ liệu thay đổi. Trong trường hợp này, hành vi tuỳ chỉnh là gọi Glide để tải một hình ảnh từ một URL vào ImageView.

  1. Mở BindingAdapters.kt. 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.
  2. Tạo một hàm bindImage() nhận ImageViewString làm tham số. Chú giải hàm này bằng @BindingAdapter. Chú giải @BindingAdapter cho biết liên kết dữ liệu mà bạn muốn phương thức điều hợp liên kết này thực thi khi một mục XML có thuộc tính imageUrl.

    Nhập androidx.databinding.BindingAdapterandroid.widget.ImageView khi có yêu cầu.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. Bên trong hàm bindImage(), hãy thêm một khối let {} cho đối số imgUrl:
imgUrl?.let { 
}
  1. Trong khối let {}, hãy thêm dòng bên dưới để chuyển đổi chuỗi URL (từ XML) thành đối tượng Uri. Nhập androidx.core.net.toUri khi được yêu cầu.

    Bạn muốn đối tượng Uri cuối cùng sử dụng lược đồ HTTPS, vì máy chủ mà bạn lấy hình ảnh yêu cầu lược đồ đó. Để sử dụng giao thức HTTPS, hãy thêm buildUpon.scheme("https") vào trình tạo toUri. Phương thức toUri() là một hàm mở rộng Kotlin trong thư viện cốt lõi Android KTX, vì vậy, phương thức này có vẻ như là một phần của lớp String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. Vẫn bên trong let {}, hãy gọi Glide.with() để tải hình ảnh từ đối tượng Uri vào ImageView. Nhập com.bumptech.glide.Glide khi có yêu cầu.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

Bước 4: Cập nhật bố cục và mảnh

Mặc dù Glide đã tải hình ảnh, nhưng vẫn chưa có gì để xem. Bước tiếp theo là cập nhật bố cục và các mảnh bằng một ImageView để hiển thị hình ảnh.

  1. Mở res/layout/gridview_item.xml. Đây là tệp tài nguyên bố cục mà bạn sẽ dùng cho từng mục trong RecyclerView sau này trong lớp học lập trình. Bạn tạm thời sử dụng bố cục này ở đây để chỉ hiển thị một hình ảnh duy nhất.
  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.marsrealestate.overview.OverviewViewModel" />
</data>
  1. 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:
app:imageUrl="@{viewModel.property.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. Đây chỉ là tạm thời; bạn sẽ quay lại bước này sau.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Thêm một dòng để tăng cường lớp GridViewItemBinding. Nhập com.example.android.marsrealestate. databinding.GridViewItemBinding khi có yêu cầu.
val binding = GridViewItemBinding.inflate(inflater)
  1. Chạy ứng dụng. Bây giờ, bạn sẽ thấy ảnh của hình ảnh từ MarsProperty đầu tiên trong danh sách kết quả.

Bước 5: Thêm hình ảnh đơn giản thể hiện lỗi và trạng thái đang tải

Glide có thể cải thiện trải nghiệm người dùng bằng cách hiển thị hình ảnh phần giữ chỗ trong khi tải hình ảnh và hình ảnh 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 và vào bố cục.

  1. Mở res/drawable/ic_broken_image.xml rồi nhấp vào thẻ Preview (Xem trước) ở 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.

  1. Mở res/drawable/loading_animation.xml. Thành phần có thể vẽ này là một ảnh động được xác định bằng thẻ <animate-rotate>. Ảnh động này làm cho hình ảnh có thể vẽ (loading_img.xml) xoay xung quanh điểm giữa. (Bạn sẽ không thấy ảnh động trong bản xem trước).

  1. Quay lại tệp BindingAdapters.kt. Trong phương thức bindImage(), hãy cập nhật lệnh gọi đến Glide.with() để gọi hàm apply() giữa load()into(). Nhập com.bumptech.glide.request.RequestOptions khi được yêu cầu.

    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_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). 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()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  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 Glide 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 phần cuối cùng của lớp học lập trình này.

Ứng dụng của bạn hiện đang tải thông tin về tài sản từ Internet. Bằng cách sử dụng dữ liệu từ mục MarsProperty đầu tiên trong danh sách, bạn đã tạo một thuộc tính LiveData trong mô hình hiển thị và bạn đã sử dụng URL hình ảnh từ dữ liệu thuộc tính đó để đ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 đó, bạn muốn sử dụng RecyclerViewGridLayoutManager.

Bước 1: Cập nhật view model

Hiện tại, mô hình chế độ xem có một _property LiveData chứa một đối tượng MarsProperty – đối tượng đầ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 đó để lưu toàn bộ danh sách đối tượng MarsProperty.

  1. Mở overview/OverviewViewModel.kt.
  2. Thay đổi biến _property riêng tư thành _properties. Thay đổi loại thành danh sách đối tượng MarsProperty.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. Thay thế dữ liệu trực tiếp property bên ngoài bằng properties. Thêm danh sách này vào loại LiveData tại đây:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. Cuộn xuống phương thức getMarsRealEstateProperties(). Bên trong khối try {}, hãy thay thế toàn bộ quy trình kiểm thử mà bạn đã thêm trong nhiệm vụ trước bằng dòng dưới đây. Vì biến listResult chứa danh sách các đối tượng MarsProperty, nên bạn chỉ cần chỉ định biến này cho _properties.value thay vì kiểm thử để có được phản hồi thành công.
_properties.value = listResult

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

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

Bước 2: Cập nhật bố cục và mảnh

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

  1. Mở res/layout/gridview_item.xml. Thay đổi hoạt động liên kết dữ liệu từ OverviewViewModel thành MarsProperty, rồi đổi tên biến thành "property".
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  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 MarsProperty:
app:imageUrl="@{property.imgSrcUrl}"
  1. Mở overview/OverviewFragment.kt. Trong 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 GridViewBinding. 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.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. Mở res/layout/fragment_overview.xml. Xoá toàn bộ phần tử <TextView>.
  2. Thay vào đó, hãy thêm phần tử <RecyclerView> này. Phần tử này sử dụng GridLayoutManager và bố cục grid_view_item cho một mục duy nhất:
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            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" />

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

Bố cục fragment_overview hiện có một RecyclerView, trong khi bố cục grid_view_item có một ImageView duy nhất. Trong bước này, bạn sẽ liên kết dữ liệu với RecyclerView thông qua một phương thức điều hợp RecyclerView.

  1. Mở overview/PhotoGridAdapter.kt.
  2. Tạo lớp PhotoGridAdapter bằng 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.

    Nhập các lớp androidx.recyclerview.widget.ListAdaptercom.example.android.marsrealestate.network.MarsProperty khi có yêu cầu. Trong các bước sau, bạn sẽ triển khai các phần 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.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. Nhấp vào vị trí bất kỳ trong lớp PhotoGridAdapter rồi nhấn Control+i để triển khai các phương thức ListAdapter, đó là onCreateViewHolder()onBindViewHolder().
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. Ở cuối phần định nghĩa lớp PhotoGridAdapter, sau các phương thức bạn vừa thêm, hãy thêm phần định nghĩa đối tượng đồng hành cho DiffCallback, như minh hoạ bên dưới.

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

    Đối tượng DiffCallback mở rộng DiffUtil.ItemCallback có loại đối tượng mà bạn muốn so sánh — MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Nhấn Control+i để triển khai các phương thức so sánh cho đối tượng này, đó là areItemsTheSame()areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. Đối với phương thức areItemsTheSame(), hãy xoá TODO. Sử dụng toán tử đẳng thức tham chiếu của Kotlin (===), toán tử này sẽ trả về true nếu các tham chiếu đối tượng cho oldItemnewItem là như nhau.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. Đối với areContentsTheSame(), hãy sử dụng toán tử đẳng thức chuẩn chỉ trên mã nhận dạng của oldItemnewItem.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. Vẫn bên trong lớp PhotoGridAdapter, bên dưới đối tượng đồng hành, hãy thêm một khai báo lớp chồng nhau cho MarsPropertyViewHolder, giúp mở rộng RecyclerView.ViewHolder.

    Nhập androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding khi được yêu cầu.

    Bạn cần biến GridViewItemBinding để liên kết MarsProperty với bố cục, vì vậy hãy truyền biến này vào MarsPropertyViewHolder. Vì lớp ViewHolder cơ sở cần có một thành phần hiển thị trong hàm khởi tạo, nên bạn sẽ truyền thành phần hiển thị gốc được liên kết vào lớp này.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. Trong MarsPropertyViewHolder, hãy tạo một phương thức bind(). Phương thức này nhận đối tượng MarsProperty 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(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. Trong onCreateViewHolder(), hãy xoá TODO và thêm dòng bên dưới. Nhập android.view.LayoutInflater khi có yêu cầu.

    Phương thức onCreateViewHolder() cần trả về một MarsPropertyViewHolder 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 MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  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 MarsProperty đượ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 MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

Bước 4: 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

Cuối cùng, hãy dùng BindingAdapter để khởi tạo PhotoGridAdapter bằng danh sách các đối tượng MarsProperty. 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 MarsProperty. Sau đó, phương thức điều hợp liên kết (binding adapter) được gọi tự động khi danh sách MarsProperty 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 MarsProperty làm đối số. Chú giải phương thức đó bằng @BindingAdapter.

    Nhập androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.network.MarsProperty khi có yêu cầu.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. Bên trong hàm bindRecyclerView(), hãy truyền recyclerView.adapter đến PhotoGridAdapter và gọi adapter.submitList() bằng dữ liệu. Thao tác này sẽ cho RecyclerView biết khi có danh sách mới.

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

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. 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.properties bằng liên kết dữ liệu.
app:listData="@{viewModel.properties}"
  1. Mở overview/OverviewFragment.kt. Trong onCreateView(), ngay trước khi gọi setHasOptionsMenu(), 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 gồm các hình ảnh MarsProperty. Khi bạn cuộn để xem hình ảnh mới, ứng dụng sẽ hiện biểu tượng đang tải trước khi hiện chính hình ảnh đó. Nếu bạn bật chế độ trên máy bay, nhữ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.

Ứng dụng MarsRealEstate 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.

Đâ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. Trong lúc ứng dụng đang tìm nạp danh sách MarsProperty, ứng dụng sẽ hiện ảnh động biểu thị trạng thái đang tải.

Bước 1: Thêm trạng thái vào mô hình hiển thị

Để bắt đầu, bạn tạo một LiveData trong mô hình hiển thị để 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 trong lệnh gọi đến await().

  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. Đổi tên cả định nghĩa dữ liệu trực tiếp _response bên trong và bên ngoài trong suốt lớp OverviewViewModel thành _status. Vì bạn đã thêm tính năng hỗ trợ cho _properties LiveData trước đó trong lớp học lập trình này, nên phản hồi đầy đủ của dịch vụ web chưa được sử dụng. Bạn cần có một LiveData ở đây để theo dõi trạng thái hiện tại, vì vậy, bạn chỉ cần đổi tên các biến hiện có.

Ngoài ra, hãy thay đổi các loại từ String thành MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. Di chuyển xuống phương thức getMarsRealEstateProperties() và cập nhật _response thành _status tại đây. Thay đổi chuỗi "Success" thành trạng thái MarsApiStatus.DONE và chuỗi "Failure" thành MarsApiStatus.ERROR.
  2. Thêm trạng thái MarsApiStatus.LOADING vào đầu khối try {}, trước khi gọi đến await(). Đây là trạng thái ban đầu khi coroutine đang chạy và bạn đang chờ dữ liệu. Khối try/catch {} hoàn chỉnh giờ đây có dạng như sau:
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} 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 _properties LiveData thành một danh sách trống. Thao tác này sẽ xoá RecyclerView.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

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

Giờ đây, bạn đã có một trạng thái trong mô hình hiển thị, nhưng đó chỉ là một tập hợp các trạng thái. Làm cách nào để thông tin này xuất hiện trong chính ứng dụng? Ở bước này, bạn sử dụng một ImageView được kết nối với tính năng liên kết dữ liệu để 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. 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ố. Nhập com.example.android.marsrealestate.overview.MarsApiStatus khi có yêu cầu.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. Thêm một 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 Glide trong nhiệm vụ trước. Nhập android.view.View khi có yêu cầu.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  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 lại đố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 tắt chế độ hiển thị của trạng thái ImageView để ẩn trạng thái đó.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Bước 3: Thêm ImageView trạng thái vào bố cục

  1. Mở res/layout/fragment_overview.xml. Bên dưới phần tử RecyclerView, bên trong ConstraintLayout, hãy thêm ImageView như dưới đây.

    ImageView này có các điều kiện ràng buộc giống 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 khung hiển thị. Ngoài ra, hãy lưu ý thuộc tính app:marsApiStatus. Thuộc tính này sẽ khiến khung hiển thị gọi BindingAdapter khi thuộc tính trạng thái trong mô hình hiển thị thay đổi.
<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}" />
  1. Bật chế độ trên máy bay trong trình mô phỏng hoặc thiết bị để mô phỏng tình trạng thiếu kết nối mạng. 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:

  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.

Dự án Android Studio: MarsRealEstateGrid

  • Để đơn giản hoá quá trình quản lý hình ảnh, hãy sử dụng thư viện Glide để 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ủa bạn.
  • Glide cần hai thứ để tải hình ảnh từ Internet: URL của hình ảnh và một đối tượng ImageView để đặt hình ảnh vào. Để chỉ định các lựa chọn này, hãy sử dụng phương thức load()into() với Glide.
  • 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 Glide để 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.
  • Để thêm các lựa chọn vào yêu cầu Glide, hãy sử dụng phương thức apply(). Ví dụ: sử dụng apply() với placeholder() để chỉ định một đối tượng có thể vẽ đang tải và sử dụng apply() với error() để chỉ định một đối tượng có thể vẽ lỗi.
  • Để tạo một lưới hình ảnh, hãy 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.

Khoá học của Udacity:

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

Khác:

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Trả lời các câu hỏi sau

Câu hỏi 1

Bạn sử dụng phương thức Glide nào để cho biết ImageView chứa hình ảnh đã tải?

into()

with()

imageview()

apply()

Câu hỏi 2

Bạn chỉ định hình ảnh của phần giữ chỗ cần hiển thị khi Glide đang tải theo cách nào?

▢ Sử dụng phương thức into() với một đối tượng có thể vẽ.

▢ Sử dụng RequestOptions() rồi gọi phương thức placeholder() bằng một đối tượng có thể vẽ.

▢ Chỉ định thuộc tính Glide.placeholder cho một đối tượng có thể vẽ.

▢ Sử dụng RequestOptions() rồi gọi phương thức loadingImage() bằng một đối tượng có thể vẽ.

Câu hỏi 3

Làm cách nào để bạn chỉ ra được một phương thức là bộ chuyển đổi liên kết?

▢ Gọi phương thức setBindingAdapter() trên LiveData.

▢ Đưa phương thức vào một tệp Kotlin có tên là BindingAdapters.kt.

▢ Sử dụng thuộc tính android:adapter trong bố cục XML.

▢ Chú giải phương thức bằng @BindingAdapter.

Bắt đầu bài học tiếp theo: 8.3 Lọc và xem chi tiết bằng dữ liệu Internet

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.