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 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 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.
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
- Mở ứng dụng giải pháp MarsPhotos từ lớp học lập trình trước.
- 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).
- Mở build.gradle (Module: app).
- 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.
- 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êmmavenCentral()
trong khốirepositories
ở trên cùng.
repositories {
google()
mavenCentral()
}
- 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.
- 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ạiMutableLiveData
, thuộc tính mới này có thể lưu trữ một đối tượngMarsPhoto
duy nhất.
private val _photos = MutableLiveData<MarsPhoto>()
Nhập com.example.android.marsphotos.network.MarsPhoto
khi có yêu cầu.
- 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ạiLiveData<MarsPhoto>
.
val photos: LiveData<MarsPhoto> = _photos
- Trong phương thức
getMarsPhotos()
, bên trong khốitry{}
, 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ànhlistResult.
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- Chỉ định ảnh chụp sao Hoả đầu tiên được truy xuất vào biến mới
_photos
. Thay đổilistResult
thành_photos.value
. Chỉ định url của hình ảnh đầu tiên tại chỉ mục0
. 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]
...
}
- 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}"
}
- 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}"
}
- 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 đượcViewModel
vàLiveData
cho URL đó.
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
- 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.
- Trong
BindingAdapters.kt
, hãy tạo một hàmbindImage()
dưới dạng hàm cấp cao nhất (không nằm trong lớp). Hàm này sẽ nhậnImageView
vàString
làm tham số.
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
Nhập android.widget.ImageView
khi có yêu cầu.
- 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ínhimageUrl
.
@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.
- Bên trong hàm
bindImage()
, hãy thêm một khốilet{}
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 {
}
- Trong khối
let{}
, hãy thêm dòng sau đây để chuyển đổi chuỗi URL thành đối tượngUri
bằng phương thứctoUri()
. Để sử dụng giao thức HTTPS, hãy thêmbuildUpon.scheme("https")
vào trình tạotoUri
. Hãy gọibuild()
để 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.
- Bên trong khối
let{}
, sau phần khai báoimgUri
, hãy sử dụngload(){}
trong Coil để tải hình ảnh từ đối tượngimgUri
vàoimgView
.
imgView.load(imgUri) {
}
Nhập coil.load
khi có yêu cầu.
- 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
.
- Mở
res/layout/grid_view_item.xml
. - 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.marsphotos.overview.OverviewViewModel" />
</data>
- 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ằngphotos
chứa danh sáchMarsPhotos
được truy xuất từ máy chủ. Gán URL đầu tiên cho thuộc tínhimageUrl
.
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.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. 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)
- Hãy sử dụng
grid_view_item.xml
thay vìfragment_overview.xml.
. Thêm dòng sau để tăng cường lớpGridViewItemBinding
.
val binding = GridViewItemBinding.inflate(inflater)
Nhập com.example.android.marsphotos. databinding.GridViewItemBinding
khi có yêu cầu.
- Chạy ứng dụng. Bây giờ, bạn sẽ thấy một ảnh chụp sao Hoả.
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.
- 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ínhandroid:tint
để làm biểu tượng có màu xám.
- 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).
- Quay lại tệp
BindingAdapters.kt
. Trong phương thứcbindImage()
, hãy cập nhật lệnh gọi thànhimgView.
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ẽ đượcloading_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ẽ đượcbroken_image
).
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_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()
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
- 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.
- 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ứconCreateview()
, hãy bỏ đánh dấu ghi chú trên dòng mã làm tăng cườngFragmentOverviewBinding
. Xoá hoặc đánh dấu ghi chú cho dòng làm tăng cườngGridViewIteMBinding
.
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
.
- Mở
overview/OverviewViewModel.kt
. - Thay đổi loại
_photos
thành danh sách đối tượngMarsPhoto
.
private val _photos = MutableLiveData<List<MarsPhoto>>()
- Thay loại
photos
của thuộc tính dự phòng thành loạiList<MarsPhoto>
:
val photos: LiveData<List<MarsPhoto>> = _photos
- Di chuyển xuống khối
try {}
bên trong phương thứcgetMarsPhotos()
.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"
- 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.
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.
- Mở
layout/grid_view_item.xml
. Xoá biến dữ liệuviewModel
. - Bên trong thẻ
<data>
, hãy thêm biếnphoto
thuộc loạiMarsPhoto
như sau.
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
- Trong
<ImageView>
, hãy thay đổi thuộc tínhapp:imageUrl
để tham chiếu đến URL hình ảnh trong đối tượngMarsPhoto
. 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}"
- Mở
layout/fragment_overview.xml
. Xoá toàn bộ phần tử<TextView>
. - Thêm phần tử
<RecyclerView>
sau. Thiết lập giá trị nhận dạng thànhphotos_grid
, thiết lập các thuộc tínhwidth
vàheight
thành0dp
để phần tử này có thể lấp đầyConstraintLayout
gốc. Bạn sẽ sử dụng bố cục Lưới, do đó, hãy thiết lập thuộc tínhlayoutManager
thànhandroidx.recyclerview.widget.GridLayoutManager
. Thiết lậpspanCount
thành2
để 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" />
- Để xem trước mã trên trong chế độ xem Design (Thiết kế), hãy sử dụng
tools:itemCount
để thiết lập16
làm số lượng mục xuất hiện trong bố cục. Thuộc tínhitemCount
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ụngtools:listitem
để thiết lậpgrid_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" />
- 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 trongrecyclerview
.
- 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ống4dp
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ụcfragment_overview.xml
và bố cụcgrid_view_item.xml
.
- Mở
layout/gridview_item.xml
. Hãy để ý thuộc tínhpadding
, bạn đã có khoảng đệm2dp
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ống4dp
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 đệm6dp
cho các cạnh bên ngoài để đúng với nguyên tắc thiết kế. - Quay lại
layout/fragment_overview.xml
. Thêm khoảng đệm6dp
choRecyclerView
. 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"
... />
- 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.
- Trong gói
overview
, hãy tạo một lớp Kotlin có tên làPhotoGridAdapter
. - 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ớpPhotoGridAdapter
mở rộngListAdapter
, 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 khaiDiffUtil.ItemCallback
.
class PhotoGridAdapter : ListAdapter<MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
Nhập các lớp androidx.recyclerview.widget.ListAdapter
và com.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.
- Để 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ứcListAdapter
,onCreateViewHolder()
và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 onCreateViewHolder
và onBindViewHolder
, bạn cần có MarsPhotoViewHolder
để thêm trong bước tiếp theo.
- Bên trong
PhotoGridAdapter
, hãy thêm một khai báo lớp chồng nhau choMarsPhotoViewHolder
, giúp mở rộngRecyclerView.ViewHolder
. Bạn cần biếnGridViewItemBinding
để liên kếtMarsPhoto
với bố cục, vì vậy hãy truyền biến này vàoMarsPhotoViewHolder
. LớpViewHolder
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.RecyclerView
và com.example.android.marsrealestate.databinding.GridViewItemBinding
nếu có yêu cầu.
- Trong
MarsPhotoViewHolder
, hãy tạo một phương thứcbind()
. Phương thức này nhận đối tượngMarsPhoto
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(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
binding.executePendingBindings()
}
- Vẫn bên trong lớp
PhotoGridAdapter
trongonCreateViewHolder()
, hãy xoá TODO và thêm dòng bên dưới. Phương thứconCreateViewHolder()
cần trả về mộtMarsPhotoViewHolder
mới được tạo bằng cách tăng cườngGridViewItemBinding
và sử dụngLayoutInflater
từ ngữ cảnhViewGroup
gốc.
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
Nhập android.view.LayoutInflater
khi có yêu cầu.
- 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ượngMarsPhoto
đượ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ứcbind()
trongMarsPhotoViewHolder
.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
- Bên trong
PhotoGridAdapter
, hãy thêm khai báo đối tượng đồng hành choDiffCallback
, như minh hoạ bên dưới.
Đối tượngDiffCallback
mở rộngDiffUtil.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.
- 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ứcareItemsTheSame()
và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") }
- Trong phương thức
areItemsTheSame()
, hãy xoáTODO
. Phương thức này đượcDiffUtil
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ượngMarsPhoto
mới có giống với đối tượngMarsPhoto
cũ hay không. Giá trị nhận dạng của mỗi mục (đối tượngMarsPhoto
) đều là duy nhất. So sánh giá trị nhận dạng củaoldItem
vànewItem
, sau đó trả về kết quả.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
- Trong
areContentsTheSame()
, hãy xoáTODO
. Phương thức này đượcDiffUtil
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ủaoldItem
vànewItem
, 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.
- Mở
BindingAdapters.kt
. - Ở cuối tệp, hãy thêm phương thức
bindRecyclerView()
nhậnRecyclerView
và danh sách đối tượngMarsPhoto
làm đối số. Chú giải phương thức đó bằng@BindingAdapter
có thuộc tínhlistData
.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
Nhập androidx.recyclerview.widget.RecyclerView
và com.example.android.marsphotos.network.MarsPhoto
nếu có yêu cầu.
- Bên trong hàm
bindRecyclerView()
, hãy truyềnrecyclerView.adapter
choPhotoGridAdapter
và chỉ định thuộc tính này cho một thuộc tínhval
mớiadapter.
val adapter = recyclerView.adapter as PhotoGridAdapter
- Ở cuối hàm
bindRecyclerView()
, hãy gọiadapter.submitList()
bằng dữ liệu về danh sách ảnh sao Hoả. Thao tác này sẽ choRecyclerView
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.
- 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)
}
- Để kết nối mọi thứ với nhau, hãy 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.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 choImageView
trong một nhiệm vụ trước đó.
app:listData="@{viewModel.photos}"
- Mở
overview/OverviewFragment.kt
. TrongonCreateView()
, ngay trước câu lệnhreturn
, hãy khởi tạo phương thức tiếp nốiRecyclerView
trongbinding.photosGrid
thành một đối tượngPhotoGridAdapter
mới.
binding.photosGrid.adapter = PhotoGridAdapter()
- 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.
- Để 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ạilayout/fragment_overview.xml
. Thêm thuộc tínhandroid:clipToPadding
choRecyclerView
, thiết lập thànhfalse
.
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- 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.
- 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.
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.
- 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.
Đâ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.
- 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 }
- Di chuyển đến phần khai báo của thuộc tính
_status
vàstatus
, thay đổi loại dữ liệu từString
thànhMarsApiStatus. 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
- Trong phương thức
getMarsPhotos()
, hãy thay đổi chuỗi"Success: ..."
thành trạng tháiMarsApiStatus.DONE
và chuỗi"Failure..."
thànhMarsApiStatus.ERROR
.
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
- Thiết lập trạng thái thành
MarsApiStatus.LOADING
phía trên khốitry {}
. Đây là trạng thái ban đầu khi coroutine đang chạy và bạn đang chờ dữ liệu. KhốiviewModelScope.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
}
}
- 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()
}
- 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.
- 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ậnImageView
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ỉnhmarsApiStatus
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.
- Thêm một khối
when {}
bên trong phương thứcbindStatus()
để chuyển đổi giữa các trạng thái.
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à ả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.
- 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áiLOADING
, hãy thiết lập trạng tháiImageView
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)
}
- 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áiImageView
thànhView.
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 đó.
- Mở
res/layout/fragment_overview.xml
. Bên trongConstraintLayout
, bên dưới phần tửRecyclerView
, hãy thêmImageView
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.
- Để 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:
- 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ã
- 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.
- 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.
- 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.
- 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)).
- 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
- Khởi động Android Studio.
- 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).
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).
- 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)).
- Nhấp đúp vào thư mục dự án đó.
- Chờ Android Studio mở dự án.
- Nhấp vào nút Run (Chạy) để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
- 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
RecyclerView
có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.
8. Tìm hiểu thêm
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: