Android Kotlin Fundamentals 08.2: Tải và hiển thị hình ảnh từ Internet

Lớp học lập trình này nằm trong khóa học về Khái niệm cơ bản về Android Kotlin. Bạn sẽ nhận được nhiều giá trị nhất từ khóa học này nếu bạn làm việc qua các lớp học lập trình theo trình tự. Tất cả các lớp học lập trình trong khóa học đều có trên trang đích của các lớp học lập trình cơ bản về Android Kotlin.

Giới thiệu

Trong lớp học lập trình trước đây, bạn đã tìm hiểu cách lấy dữ liệu từ 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 trên web. Bạn cũng ô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.

Những điều bạn nên biết

  • Cách tạo và sử dụng các mảnh (fragment).
  • Cách sử dụng các thành phần cấu trúc bao gồm mô hình chế độ xem, nhà máy mô hình, phép biến đổi và LiveData.
  • Cách truy xuất JSON từ dịch vụ web REST và phân tích cú pháp dữ liệu đó vào các đối tượng 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 từ mộ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.

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng MarsRealEstate để lấy URL hình ảnh từ dữ liệu thuộc tính Mars, và 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.
  • Sử dụng RecyclerView để hiển thị lưới các hình ảnh tài sản Mars.
  • 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 làm việc với một ứng dụng có tên MarsRealEstate, trong đó hiển thị các tài sản để bán trên Mars. Ứng dụng sẽ kết nối với một máy chủ Internet để truy xuất và hiển thị dữ liệu của cơ sở lưu trú, bao gồm cả thông tin chi tiết như giá và liệu khách sạn có thể bán hoặc cho thuê hay không. Hình ảnh đại diện cho mỗi cơ sở là ảnh thực tế từ sao Hỏa được chụp từ xe tự hành của Sao Hỏa

Phiên bản của ứng dụng bạn tạo trong lớp học lập trình này sẽ lấp đầy trang tổng quan và hiển thị lưới dạng hình ảnh. Hình ảnh là một phần của dữ liệu tài sản mà ứng dụng của bạn nhận được từ dịch vụ web bất động sản Mars. Ứng dụng của bạn sẽ dùng thư viện Glide để tải và hiển thị hình ảnh, cũng như 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 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 vào bộ đệ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. Việc tìm hiểu cách tải hiệu quả hình ảnh từ mạng có thể là lớp học lập trình.

Rất may là bạn có thể sử dụng thư viện do cộng đồng phát triển có tên Glide để tải xuống, lưu vào bộ đệm, giải mã và lưu vào bộ nhớ đệm các hình ảnh của mình. Glide cung cấp cho bạn ít công việc hơn rất nhiều so với khi bạn phải làm tất cả những việc này từ đầu.

Về cơ bản, Glide có hai thứ:

  • URL của hình ảnh bạn muốn tải và hiển thị.
  • Đối tượng ImageView để hiển thị hình ảnh đó.

Trong nhiệm vụ này, bạn tìm hiểu cách sử dụng Glide để hiển thị một hình ảnh duy nhất từ dịch vụ web bất động sản. Bạn sẽ hiển thị hình ảnh đại diện cho tài sản Mars đầu tiên trong danh sách các 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 của Glide

  1. Mở ứng dụng MarsRealEstate từ lớp học lập trình cuối cùng. (Bạn có thể tải MarsRealEstateNetwork xuống đây nếu không có ứng dụng.)
  2. Chạy ứng dụng để xem cách hoạt động. (Chế độ này hiển thị thông tin chi tiết bằng văn bản của một cơ sở lưu trú giả định trên Sao Hỏa.)
  3. Mở build.gradle (Module: app).
  4. Trong phần dependencies, hãy thêm dòng này cho 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 mô hình chế độ xem

Tiếp theo, bạn cập nhật lớp OverviewViewModel để bao gồm dữ liệu trực tiếp cho một thuộc tính Mars.

  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.

    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 bên trong khối try/catch {}, đặt _response.value thành số lượng thuộc tính. Thêm thử nghiệm dưới đây. Nếu có các đối tượng MarsProperty, thì thử nghiệm 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 Mars đầu tiên. Tất cả những gì bạn đã làm cho đến nay đã được thiết lập mô hình chế độ xem và dữ liệu trực tiếp cho URL đó.

Bước 3: Tạo bộ chuyển đổi liên kết và gọi Glide

Bây giờ, bạn có URL của hình ảnh cần 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 dùng bộ chuyển đổi liên kết để lấy URL từ thuộc tính XML liên kết với ImageView và dùng chế độ Glide để tải hình ảnh. Bộ chuyển đổi liên kết là phương thức mở rộng đặt giữa chế độ xem và dữ liệu ràng buộc để cung cấp hành vi tùy chỉnh khi dữ liệu thay đổi. Trong trường hợp này, hành vi tùy chỉnh là gọi Glide để tải một hình ảnh từ một URL vào một 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 ImageView và một String làm thông số. Chú thích hàm bằng @BindingAdapter. Chú thích @BindingAdapter cho biết liên kết dữ liệu mà bạn muốn thực thi bộ chuyển đổi liên kết này 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. Bên 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. Hãy 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 cần phải có lược đồ đó. Để sử dụng lượ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 từ thư viện lõi Android KTX, vì vậy, có vẻ như hàm này là một phần của lớp String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. Vẫn ở trong let {}, 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 chưa có dữ liệu nào để xem. Bước tiếp theo là cập nhật bố cục và các mảnh bằng 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 bạn sẽ sử dụng cho từng mục trong RecyclerView sau trong lớp học lập trình. Bạn tạm thời dùng mã này tại đâ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. Hãy thêm thuộc tính app:imageUrl vào phần tử ImageView để sử dụng bộ chuyển đổi liên kết 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 sau.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Hãy 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 tải và lỗi đơn giản

Glide có thể cải thiện trải nghiệm của người dùng bằng cách hiển thị hình ảnh giữ chỗ trong khi tải hình ảnh và hình ảnh lỗi nếu quá trình tải không thành công, chẳng hạn như nếu hình ảnh bị thiếu hoặc bị hỏng. Ở bước này, bạn thêm chức năng đó vào bộ chuyển đổi liên kết và vào bố cục.

  1. Mở res/drawable/ic_broken_image.xml và nhấp vào thẻ 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. Hình vẽ này là một ảnh động được xác định bằng thẻ <animate-rotate>. Ảnh động này xoay một hình ảnh có thể vẽ, loading_img.xml, xung quanh điểm chính giữa. (Bạn không thấy hiệu ứng độ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 thành Glide.with() để gọi hàm apply() từ load() đến into(). Nhập com.bumptech.glide.request.RequestOptions khi được yêu cầu.

    Mã này đặt hình ảnh tải phần giữ chỗ để sử dụng trong khi tải (loading_animation có thể vẽ). Mã này cũng đặt hình ảnh để sử dụng nếu không tải được hình ảnh (broken_image có thể vẽ). Phương thức bindImage() hoàn chỉnh hiện 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. Tùy thuộc vào tốc độ kết nối mạng của bạn, bạn có thể thấy hình ảnh tải nhanh trong khi Glide tải xuống và hiển thị hình ảnh thuộc tính. Nhưng bạn sẽ không thấy biểu tượng hình ảnh bị hỏng, ngay cả khi bạn tắt mạng — bạn khắc phục điều đó trong phần cuối cùng của lớp học lập trình.

Ứng dụng của bạn hiện đang tải thông tin tài sản từ Internet. Bạn đang sử dụng dữ liệu từ mục danh sách MarsProperty đầu tiên, bạn đã tạo một tài sản LiveData trong mô hình chế độ xem và sử dụng URL hình ảnh từ dữ liệu tài sản đó để điền ImageView. Tuy nhiên, mục tiêu là ứng dụng của bạn hiển thị lưới hình ảnh, vì vậy, bạn muốn sử dụng RecyclerView với GridLayoutManager.

Bước 1: Cập nhật mô hình chế độ xem

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 của dịch vụ web. Trong bước này, bạn thay đổi LiveData để lưu giữ 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 các đối tượng MarsProperty.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. Thay thế dữ liệu trực tiếp bên ngoài property bằng properties. Ngoài ra, hãy thêm danh sách này vào loại LiveData tại đây:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. Di chuyển xuống phương thức getMarsRealEstateProperties(). Bên trong khối try {}, hãy thay thế toàn bộ thử nghiệm mà bạn đã thêm vào việc cần làm trước đó bằng dòng hiển thị bên dưới. Vì biến listResult có 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ì thử nghiệm để 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 chế độ xem tuần hoàn và bố cục lưới, thay vì chế độ xem hình ảnh đơn lẻ.

  1. Mở res/layout/gridview_item.xml. Thay đổi liên kết dữ liệu từ OverviewViewModel thành MarsProperty và đổ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 của 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 dòng làm tă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ẽ hủy các thay đổi tạm thời mà bạn đã thực hiện trong việc cần làm cuối cùng.
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. Hãy thêm phần tử <RecyclerView> này (sử dụng bố cục GridLayoutManager và bố cục grid_view_item cho một mục):
<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 bộ chuyển đổi lưới ảnh

Bây giờ, bố cục fragment_overviewRecyclerView, còn bố cục grid_view_item chỉ có một ImageView. Trong bước này, bạn liên kết dữ liệu với RecyclerView thông qua một bộ chuyển đổi RecyclerView.

  1. Mở overview/PhotoGridAdapter.kt.
  2. Tạo lớp PhotoGridAdapter, với các thông số hàm dựng được hiển thị bên dưới. Lớp PhotoGridAdapter mở rộng ListAdapter. Lớp này cần có loại mục danh sách, chủ sở hữu chế độ xem và cách 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 đây, bạn triển khai các phần còn thiếu khác của hàm dựng đang tạo 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 vào Control+i để triển khai các phương thức ListAdapter, tức 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 định nghĩa lớp PhotoGridAdapter, sau các phương thức mà bạn vừa thêm, hãy thêm định nghĩa đối tượng companion cho DiffCallback, như minh họa 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 với loại đối tượng bạn muốn so sánh – MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Hãy 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 xóa VIỆC CẦN LÀM. Dùng toán tử đẳng thức tham chiếu (===) của Kotlin, trả về true nếu đối tượng tham chiếu đến oldItemnewItem giống nhau.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. Đối với areContentsTheSame(), chỉ dùng toán tử đẳng thức chuẩn trên mã 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 companion, hãy thêm định nghĩa lớp bên trong cho MarsPropertyViewHolder, trong đó 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 có biến GridViewItemBinding để liên kết MarsProperty với bố cục, vì vậy hãy chuyển biến vào MarsPropertyViewHolder. Vì lớp ViewHolder cơ sở yêu cầu một chế độ xem trong hàm dựng, nên bạn sẽ chuyển chế độ xem gốc liên kết đó.
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 xóa VIỆC CẦN LÀM và thêm dòng được hiển thị 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 của cha mẹ.
   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 đó chuyển thuộc tính đó sang phương thức bind() trong MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

Bước 4: Thêm bộ chuyển đổi liên kết và kết nối các bộ phận

Cuối cùng, hãy dùng BindingAdapter để khởi tạo PhotoGridAdapter với 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ú thích 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ó một 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 lệnh gọi tới setHasOptionsMenu(), hãy khởi chạy bộ chuyển đổi RecyclerView trong binding.photosGrid thành đối tượng PhotoGridAdapter mới.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. Chạy ứng dụng. Bạn sẽ thấy lưới gồm MarsProperty hình ảnh. Khi bạn cuộn để xem hình ảnh mới, ứng dụng sẽ hiển thị biểu tượng tiến trình tải trước khi hiển thị hình ảnh. Nếu bạn bật chế độ trên máy bay, thì 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 thị 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 tuyệt vời cho người dùng. Trong nhiệm vụ này, bạn thêm chức năng xử lý lỗi cơ bản để giúp người dùng hiểu rõ hơn. Nếu không có Internet, ứng dụng sẽ hiển thị biểu tượng lỗi kết nối. Trong khi tìm nạp danh sách MarsProperty, ứng dụng sẽ hiển thị ảnh động đang tải.

Bước 1: Thêm trạng thái vào mô hình chế độ xem

Để bắt đầu, bạn tạo một LiveData trong mô hình chế độ xem để đại diện cho 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 tải xảy ra trong khi bạn chờ dữ liệu trong cuộc 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 bên trong và bên ngoài _response trong suốt lớp OverviewViewModel thành _status. Vì bạn đã thêm hỗ trợ cho _properties LiveData trước đó trong lớp học lập trình này nên phản hồi dịch vụ web hoàn chỉnh chưa được sử dụng. Bạn cần có LiveData ở đây để theo dõi trạng thái hiện tại, như 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. Hãy 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 khi trạng thái lỗi trong khối catch {}, hãy đặt _properties LiveData thành danh sách trống. Thao tác này sẽ xóa RecyclerView.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

Bước 2: Thêm bộ chuyển đổi liên kết cho trạng thái Chế độ xem hình ảnh

Bây giờ, bạn đã có trạng thái trong mô hình chế độ xem, nhưng chỉ có một nhóm trạng thái. Làm thế nào để nội dung đó xuất hiện trong chính ứng dụng? Ở bước này, bạn sử dụng ImageView, được kết nối với liên kết dữ liệu, để hiển thị các biểu tượng cho các trạng thái tải và lỗi. Khi ứng dụng ở trạng thái tải hoặc trạng thái tải, ImageView sẽ hiển thị. Khi ứng dụng tải xong, ImageView sẽ không hiển thị.

  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 {} vào trong phương thức bindStatus() để chuyển đổi giữa các trạng thái khác nhau.
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à hình vẽ hoạt hình mà bạn đã 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ư việc bạn đã làm đối với trạng thái LOADING, hãy đặt trạng thái ImageView thành hiển thị và sử dụng lại lỗi kết nối có thể vẽ.
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. Tại đây, bạn đã 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 câu trả lời đó.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Bước 3: Thêm Chế độ xem hình ảnh 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 hiển thị bên dưới.

    ImageView này có các hạn chế 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 chế độ xem. Ngoài ra, hãy chú ý đến thuộc tính app:marsApiStatus. Thuộc tính này có lệnh gọi BindingAdapter khi thuộc tính trạng thái trong mô hình chế độ xem 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. Hãy bật chế độ trên máy bay trong trình mô phỏng hoặc thiết bị để mô phỏng một tình trạng kết nối mạng bị thiếu. 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 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 hóa quá trình quản lý hình ảnh, hãy sử dụng thư viện 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 trong ứng dụng của bạn.
  • Glide có hai yếu tố để tải hình ảnh từ Internet: URL của hình ảnh và đối tượng ImageView để đặt hình ảnh vào. Để chỉ định những tùy chọn này, hãy sử dụng phương thức load()into() bằng cách 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ị đó. Bộ chuyển đổi liên kết cung cấp hành vi tùy chỉnh khi dữ liệu thay đổi, ví dụ: để gọi Glide nhằm tải một hình ảnh từ một URL vào một 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 tùy chọn vào yêu cầu Glide, hãy sử dụng phương thức apply(). Ví dụ: dùng apply() với placeholder() để chỉ định chế độ tải có thể vẽ và dùng apply() với error() để chỉ định lỗi có thể vẽ.
  • Để tạo lưới hình ảnh, hãy sử dụng RecyclerView với GridLayoutManager.
  • Để 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.

Khóa học từ Udacity:

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

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

Phần này liệt kê các bài tập về nhà có thể được giao cho học viên đang làm việc qua lớp học lập trình này trong khóa học do người hướng dẫn tổ chức. Người hướng dẫn có thể làm những việc sau:

  • Giao bài tập về nhà nếu được yêu cầu.
  • Trao đổi với học viên 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 những đề xuất này ít hay nhiều tùy ý. Do đó, họ có thể thoải mái giao 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ự mình làm việc qua lớp học lập trình này, hãy thoải mái sử dụng các bài tập về nhà này để kiểm tra kiến thức của bạn.

Trả lời những câu hỏi này

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 tài nguyên có thể vẽ.

▢ Sử dụng RequestOptions() và gọi phương thức placeholder() có thể vẽ.

▢ Chỉ định thuộc tính Glide.placeholder cho một tài sản có thể vẽ.

▢ Sử dụng RequestOptions() và gọi phương thức loadingImage() 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.

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

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

▢ Chú thích phương thức bằng @BindingAdapter.

Bắt đầu bài học tiếp theo: 8.3 Lọc và chế độ xem chi tiết với 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 khóa học này, hãy xem trang đích của các lớp học lập trình cơ bản về Android Kotlin.