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 Retrofit và Moshi.
- Biết cách tạo bố cục lưới bằng
RecyclerView
. - Biết cách hoạt động của
Adapter
,ViewHolder
vàDiffUtil
.
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
- 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.)
- 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.)
- Mở build.gradle (Module: app).
- 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.
- 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.
- Mở
overview/OverviewViewModel.kt
. Ngay bên dướiLiveData
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ượngMarsProperty
.
Nhập lớpMarsProperty
(com.example.android.marsrealestate.network.MarsProperty
) khi được yêu cầu.
private val _property = MutableLiveData<MarsProperty>()
val property: LiveData<MarsProperty>
get() = _property
- Trong phương thức
getMarsRealEstateProperties()
, hãy tìm dòng bên trong khốitry/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ượngMarsProperty
, thì thử nghiệm này sẽ đặt giá trị của_property
LiveData
thành thuộc tính đầu tiên tronglistResult
.
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}"
}
- Mở tệp
res/layout/fragment_overview.xml
. Trong phần tử<TextView>
, hãy thay đổiandroid:text
để liên kết với thành phầnimgSrcUrl
củaproperty
LiveData
:
android:text="@{viewModel.property.imgSrcUrl}"
- 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
.
- 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. - Tạo một hàm
bindImage()
nhậnImageView
và mộtString
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ínhimageUrl
.
Nhậpandroidx.databinding.BindingAdapter
vàandroid.widget.ImageView
khi được yêu cầu.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
- Bên trong hàm
bindImage()
, hãy thêm một khốilet {}
cho đối sốimgUrl
:
imgUrl?.let {
}
- 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ượngUri
. Hãy nhậpandroidx.core.net.toUri
khi được yêu cầu.
Bạn muốn đối tượngUri
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êmbuildUpon.scheme("https")
vào trình tạotoUri
. Phương thứctoUri()
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ớpString
.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
- Vẫn ở trong
let {}
, gọiGlide.with()
để tải hình ảnh từ đối tượngUri
vàoImageView
. Nhậpcom.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.
- 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 trongRecyclerView
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. - 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ớpOverviewViewModel
:
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
- 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}"
- Mở
overview/OverviewFragment.kt
. Trong phương thứconCreateView()
, hãy đánh dấu ghi chú vào dòng mã làm tăng cường lớpFragmentOverviewBinding
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)
- Hãy thêm một dòng để tăng cường lớp
GridViewItemBinding
. Nhậpcom.example.android.marsrealestate. databinding.GridViewItemBinding
khi có yêu cầu.
val binding = GridViewItemBinding.inflate(inflater)
- 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.
- 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ínhandroid:tint
để làm biểu tượng có màu xám.
- 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).
- Quay lại tệp
BindingAdapters.kt
. Trong phương thứcbindImage()
, hãy cập nhật lệnh gọi thànhGlide.with()
để gọi hàmapply()
từload()
đếninto()
. Nhậpcom.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ứcbindImage()
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)
}
}
- 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
.
- Mở
overview/OverviewViewModel.kt
. - Thay đổi biến
_property
riêng tư thành_properties
. Thay đổi loại thành danh sách các đối tượngMarsProperty
.
private val _properties = MutableLiveData<List<MarsProperty>>()
- Thay thế dữ liệu trực tiếp bên ngoài
property
bằngproperties
. Ngoài ra, hãy thêm danh sách này vào loạiLiveData
tại đây:
val properties: LiveData<List<MarsProperty>>
get() = _properties
- Di chuyển xuống phương thức
getMarsRealEstateProperties()
. Bên trong khốitry {}
, 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ếnlistResult
có danh sách các đối tượngMarsProperty
, 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ẻ.
- Mở
res/layout/gridview_item.xml
. Thay đổi liên kết dữ liệu từOverviewViewModel
thànhMarsProperty
và đổi tên biến thành"property"
.
<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
- Trong
<ImageView>
, hãy thay đổi thuộc tínhapp:imageUrl
để tham chiếu đến URL của hình ảnh trong đối tượngMarsProperty
:
app:imageUrl="@{property.imgSrcUrl}"
- Mở
overview/OverviewFragment.kt
. TrongonCreateview()
, hãy bỏ đánh dấu dòng làm tăngFragmentOverviewBinding
. Xoá hoặc đánh dấu ghi chú cho dòng làm tăng cườngGridViewBinding
. 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)
- Mở
res/layout/fragment_overview.xml
. Xoá toàn bộ phần tử<TextView>
. - Hãy thêm phần tử
<RecyclerView>
này (sử dụng bố cụcGridLayoutManager
và bố cụcgrid_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_overview
có RecyclerView
, 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
.
- Mở
overview/PhotoGridAdapter.kt
. - 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ớpPhotoGridAdapter
mở rộngListAdapter
. 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 khaiDiffUtil.ItemCallback
.
Nhập các lớpandroidx.recyclerview.widget.ListAdapter
vàcom.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) {
}
- Nhấp vào vị trí bất kỳ trong lớp
PhotoGridAdapter
rồi nhấn vàoControl+i
để triển khai các phương thứcListAdapter
, tức làonCreateViewHolder()
vàonBindViewHolder()
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
TODO("not implemented")
}
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
TODO("not implemented")
}
- Ở 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 choDiffCallback
, như minh họa bên dưới.
Nhậpandroidx.recyclerview.widget.DiffUtil
khi được yêu cầu.
Đối tượngDiffCallback
mở rộngDiffUtil.ItemCallback
với loại đối tượng bạn muốn so sánh –MarsProperty
.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
- 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()
vàareContentsTheSame()
.
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
TODO("not implemented")
}
override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
TODO("not implemented") }
- Đố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 đếnoldItem
vànewItem
giống nhau.
override fun areItemsTheSame(oldItem: MarsProperty,
newItem: MarsProperty): Boolean {
return oldItem === newItem
}
- Đối với
areContentsTheSame()
, chỉ dùng toán tử đẳng thức chuẩn trên mãoldItem
vànewItem
.
override fun areContentsTheSame(oldItem: MarsProperty,
newItem: MarsProperty): Boolean {
return oldItem.id == newItem.id
}
- 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 choMarsPropertyViewHolder
, trong đó mở rộngRecyclerView.ViewHolder
.
Nhậpandroidx.recyclerview.widget.RecyclerView
vàcom.example.android.marsrealestate.databinding.GridViewItemBinding
khi được yêu cầu.
Bạn cần có biếnGridViewItemBinding
để liên kếtMarsProperty
với bố cục, vì vậy hãy chuyển biến vàoMarsPropertyViewHolder
. Vì lớpViewHolder
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) {
}
- Trong
MarsPropertyViewHolder
, hãy tạo một phương thứcbind()
. Phương thức này nhận đối tượngMarsProperty
làm đối số và thiết lậpbinding.property
thành đối tượng đó. GọiexecutePendingBindings()
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()
}
- 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ậpandroid.view.LayoutInflater
khi có yêu cầu.
Phương thứconCreateViewHolder()
cần trả về mộtMarsPropertyViewHolder
mới, được tạo bằng cách tăng cườngGridViewItemBinding
và sử dụngLayoutInflater
từ ngữ cảnhViewGroup
của cha mẹ.
return MarsPropertyViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
- 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ọigetItem()
để lấy đối tượngMarsProperty
đượ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ứcbind()
trongMarsPropertyViewHolder
.
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.
- Mở
BindingAdapters.kt
. - Ở cuối tệp, hãy thêm phương thức
bindRecyclerView()
nhậnRecyclerView
và danh sách đối tượngMarsProperty
làm đối số. Chú thích phương thức đó bằng@BindingAdapter
.
Nhậpandroidx.recyclerview.widget.RecyclerView
vàcom.example.android.marsrealestate.network.MarsProperty
khi được yêu cầu.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsProperty>?) {
}
- Bên trong hàm
bindRecyclerView()
, hãy truyềnrecyclerView.adapter
đếnPhotoGridAdapter
và gọiadapter.submitList()
bằng dữ liệu đó. Thao tác này sẽ choRecyclerView
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)
- Mở
res/layout/fragment_overview.xml
. Thêm thuộc tínhapp:listData
vào phần tửRecyclerView
và thiết lập thànhviewmodel.properties
bằng liên kết dữ liệu.
app:listData="@{viewModel.properties}"
- Mở
overview/OverviewFragment.kt
. TrongonCreateView()
, ngay trước lệnh gọi tớisetHasOptionsMenu()
, hãy khởi chạy bộ chuyển đổiRecyclerView
trongbinding.photosGrid
thành đối tượngPhotoGridAdapter
mới.
binding.photosGrid.adapter = PhotoGridAdapter()
- 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()
.
- 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êmenum
để biểu thị tất cả trạng thái đang có:
enum class MarsApiStatus { LOADING, ERROR, DONE }
- Đổ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ớpOverviewViewModel
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
- 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áiMarsApiStatus.DONE
và chuỗi"Failure"
thànhMarsApiStatus.ERROR
. - Thêm trạng thái
MarsApiStatus.LOADING
vào đầu khốitry {}
, trước khi gọi đếnawait()
. Đây là trạng thái ban đầu khi coroutine đang chạy và bạn đang chờ dữ liệu. Khốitry/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
}
- 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óaRecyclerView
.
} 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ị.
- 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ậnImageView
và giá trịMarsApiStatus
làm đối số. Nhậpcom.example.android.marsrealestate.overview.MarsApiStatus
khi được yêu cầu.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
- Thêm một
when {}
vào trong phương thứcbindStatus()
để chuyển đổi giữa các trạng thái khác nhau.
when (status) {
}
- 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ậpImageView
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ậpandroid.view.View
khi được yêu cầu.
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
- 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áiLOADING
, hãy đặt trạng tháiImageView
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)
}
- 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áiImageView
để ẩ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
- Mở
res/layout/fragment_overview.xml
. Bên dưới phần tửRecyclerView
, bên trongConstraintLayout
, hãy thêmImageView
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ụngwrap_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ínhapp:marsApiStatus
. Thuộc tính này có lệnh gọiBindingAdapter
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}" />
- 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:
- 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ứcload()
và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ùngapply()
vớiplaceholder()
để chỉ định chế độ tải có thể vẽ và dùngapply()
vớierror()
để chỉ định lỗi có thể vẽ. - Để tạo lưới hình ảnh, hãy sử dụng
RecyclerView
vớiGridLayoutManager
. - Để 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:
- Tổng quan về ViewModel
- Tổng quan về LiveData
- Coroutine, tài liệu chính thức
- Các phương thức điều hợp liên kết (binding adapter)
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:
Để 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.