Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.
Giới thiệu
Trong các lớp học lập trình trước của bài học này, bạn đã tìm hiểu cách lấy dữ liệu về bất động sản trên sao Hoả từ một dịch vụ web và cách tạo một RecyclerView có bố cục dạng lưới để tải và hiển thị hình ảnh từ dữ liệu đó. Trong lớp học lập trình này, bạn sẽ hoàn thiện ứng dụng MarsRealEstate bằng cách triển khai khả năng lọc các tài sản trên sao Hoả theo trạng thái cho thuê hoặc bán. Bạn cũng tạo một chế độ xem chi tiết để nếu người dùng nhấn vào ảnh của một tài sản trong chế độ xem tổng quan, họ sẽ thấy một chế độ xem chi tiết có thông tin chi tiết về tài sản đó.
Kiến thức bạn cần có
- Cách tạo và sử dụng các mảnh.
- Cách điều hướng giữa các mảnh và sử dụng Safe Args (một trình bổ trợ Gradle) để truyền dữ liệu giữa các mảnh.
- Cách sử dụng các thành phần cấu trúc, bao gồm cả mô hình hiển thị, nhà máy mô hình hiển thị, các phép biến đổi và
LiveData. - Cách truy xuất dữ liệu được mã hoá 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 Kotlin bằng thư viện Retrofit và Moshi.
Kiến thức bạn sẽ học được
- Cách sử dụng các biểu thức liên kết phức tạp trong tệp bố cục.
- Cách gửi yêu cầu Retrofit đến một dịch vụ web có các lựa chọn truy vấn.
Bạn sẽ thực hiện
- Sửa đổi ứng dụng MarsRealEstate để đánh dấu những tài sản trên sao Hoả đang được bán (thay vì cho thuê) bằng biểu tượng ký hiệu đô la.
- Sử dụng trình đơn tuỳ chọn trên trang tổng quan để tạo một yêu cầu dịch vụ web nhằm lọc các tài sản trên sao Hoả theo loại.
- Tạo một mảnh chi tiết cho một tài sản trên Sao Hoả, liên kết mảnh đó với lưới tổng quan bằng chế độ điều hướng và truyền dữ liệu tài sản vào mảnh đó.
Trong lớp học lập trình này (và các lớp học lập trình liên quan), bạn sẽ làm việc với một ứng dụng có tên là MarsRealEstate, ứng dụng này đăng tin rao bán bất động sản trên sao Hoả. Ứng dụng này kết nối với một máy chủ Internet để truy xuất và hiển thị dữ liệu về tài sản, bao gồm cả các thông tin chi tiết như giá và việc tài sản có đang được bán hoặc cho thuê hay không. Những hình ảnh đại diện cho từng đối tượng là ảnh chụp sao Hoả thực tế được chụp qua thiết bị thám hiểm sao Hoả của NASA. Trong các lớp học lập trình trước, bạn đã tạo một RecyclerView có bố cục dạng lưới cho tất cả ảnh của cơ sở lưu trú:

Trong phiên bản này của ứng dụng, bạn sẽ làm việc với loại tài sản (thuê so với mua) và thêm một biểu tượng vào bố cục dạng lưới để đánh dấu những tài sản đang được bán:

Bạn sửa đổi trình đơn tuỳ chọn của ứng dụng để lọc lưới nhằm chỉ hiển thị những tài sản cho thuê hoặc để bán:

Cuối cùng, bạn tạo một chế độ xem chi tiết cho từng tài sản riêng lẻ và kết nối các biểu tượng trên lưới tổng quan với mảnh chi tiết đó bằng hệ thống điều hướng:

Cho đến nay, phần duy nhất trong dữ liệu về tài sản trên sao Hoả mà bạn đã sử dụng là URL cho hình ảnh của tài sản. Nhưng dữ liệu về tài sản (mà bạn đã xác định trong lớp MarsProperty) cũng bao gồm mã nhận dạng, giá và loại (cho thuê hoặc bán). Để giúp bạn nhớ lại, đây là một đoạn mã dữ liệu JSON mà bạn nhận được từ dịch vụ web:
{
"price":8000000,
"id":"424908",
"type":"rent",
"img_src": "http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631290305226E03_DXXX.jpg"
},Trong nhiệm vụ này, bạn sẽ bắt đầu làm việc với kiểu tài sản trên sao Hoả để thêm hình ảnh dấu đô la vào những tài sản đang được bán trên trang tổng quan.
Bước 1: Cập nhật MarsProperty để thêm loại
Lớp MarsProperty xác định cấu trúc dữ liệu cho từng thuộc tính do dịch vụ web cung cấp. Trong một lớp học lập trình trước, bạn đã sử dụng thư viện Moshi để phân tích cú pháp phản hồi JSON thô từ dịch vụ web sao Hoả thành các đối tượng dữ liệu MarsProperty riêng lẻ.
Trong bước này, bạn sẽ thêm một số logic vào lớp MarsProperty để cho biết một tài sản có cho thuê hay không (tức là loại có phải là chuỗi "rent" hay "buy"). Bạn sẽ sử dụng logic này ở nhiều nơi, vì vậy, tốt hơn là bạn nên có logic này ở đây trong lớp dữ liệu thay vì sao chép.
- Mở ứng dụng MarsRealEstate từ lớp học lập trình gần đây nhất. (Bạn có thể tải MarsRealEstateGrid xuống nếu chưa có ứng dụng này.)
- Mở
network/MarsProperty.kt. Thêm một phần nội dung vào định nghĩa lớpMarsPropertyvà thêm một phương thức getter tuỳ chỉnh choisRentaltrả vềtruenếu đối tượng thuộc kiểu"rent".
data class MarsProperty(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double) {
val isRental
get() = type == "rent"
}Bước 2: Cập nhật bố cục của mục trong lưới
Giờ đây, bạn sẽ cập nhật bố cục mục cho lưới hình ảnh để chỉ hiện một drawable có ký hiệu đô la trên những hình ảnh tài sản đang được bán:

Với các biểu thức liên kết dữ liệu, bạn có thể thực hiện kiểm thử này hoàn toàn trong bố cục XML cho các mục trong lưới.
- Mở
res/layout/grid_view_item.xml. Đây là tệp bố cục cho từng ô riêng lẻ trong bố cục lưới củaRecyclerView. Hiện tại, tệp chỉ chứa phần tử<ImageView>cho hình ảnh của tài sản. - Bên trong phần tử
<data>, hãy thêm một phần tử<import>cho lớpView. Bạn dùng các lệnh nhập khi muốn sử dụng các thành phần của một lớp trong biểu thức liên kết dữ liệu trong tệp bố cục. Trong trường hợp này, bạn sẽ sử dụng các hằng sốView.GONEvàView.VISIBLE, vì vậy, bạn cần có quyền truy cập vào lớpView.
<import type="android.view.View"/>- Bao quanh toàn bộ khung hiển thị hình ảnh bằng một
FrameLayoutđể cho phép thành phần có thể vẽ dấu đô la được xếp chồng lên trên hình ảnh tài sản.
<FrameLayout
android:layout_width="match_parent"
android:layout_height="170dp">
<ImageView
android:id="@+id/mars_image"
...
</FrameLayout>- Đối với
ImageView, hãy thay đổi thuộc tínhandroid:layout_heightthànhmatch_parentđể điền vàoFrameLayoutgốc mới.
android:layout_height="match_parent"- Thêm phần tử
<ImageView>thứ hai ngay bên dưới phần tử đầu tiên, bên trongFrameLayout. Hãy sử dụng định nghĩa được trình bày bên dưới. Hình ảnh này xuất hiện ở góc dưới cùng bên phải của mục trong lưới, ở trên cùng của hình ảnh Sao Hoả và sử dụng đối tượng có thể vẽ được xác định trongres/drawable/ic_for_sale_outline.xmlcho biểu tượng dấu đô la.
<ImageView
android:id="@+id/mars_property_type"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_gravity="bottom|end"
android:adjustViewBounds="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_for_sale_outline"
tools:src="@drawable/ic_for_sale_outline"/>- Thêm thuộc tính
android:visibilityvào khung hiển thị hình ảnhmars_property_type. Sử dụng một biểu thức liên kết để kiểm tra loại tài sản và chỉ định chế độ hiển thị choView.GONE(đối với nội dung thuê) hoặcView.VISIBLE(đối với nội dung mua).
android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"Cho đến nay, bạn chỉ thấy các biểu thức liên kết trong những bố cục sử dụng các biến riêng lẻ được xác định trong phần tử <data>. Biểu thức liên kết rất hữu hiệu và cho phép bạn thực hiện các thao tác như kiểm thử và tính toán hoàn toàn trong bố cục XML. Trong trường hợp này, bạn dùng toán tử ba ngôi (?:) để thực hiện một kiểm thử (đây có phải là đối tượng cho thuê không?). Bạn cung cấp một kết quả cho giá trị true (ẩn biểu tượng dấu đô la bằng View.GONE) và một kết quả khác cho giá trị false (hiện biểu tượng đó bằng View.VISIBLE).
Tệp grid_view_item.xml hoàn chỉnh mới được minh hoạ bên dưới:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="170dp">
<ImageView
android:id="@+id/mars_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:padding="2dp"
app:imageUrl="@{property.imgSrcUrl}"
tools:src="@tools:sample/backgrounds/scenic"/>
<ImageView
android:id="@+id/mars_property_type"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_gravity="bottom|end"
android:adjustViewBounds="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_for_sale_outline"
android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"
tools:src="@drawable/ic_for_sale_outline"/>
</FrameLayout>
</layout>- Biên dịch và chạy ứng dụng, đồng thời lưu ý rằng những tài sản không phải là tài sản cho thuê sẽ có biểu tượng dấu đô la.

Hiện tại, ứng dụng của bạn hiển thị tất cả các tài sản trên sao Hoả trong lưới tổng quan. Nếu người dùng đang mua sắm một tài sản cho thuê trên Sao Hoả, thì việc có các biểu tượng cho biết tài sản nào đang được bán sẽ rất hữu ích, nhưng vẫn còn rất nhiều tài sản để người dùng cuộn qua trên trang. Trong nhiệm vụ này, bạn sẽ thêm một trình đơn tuỳ chọn vào mảnh tổng quan để cho phép người dùng chỉ xem các tài sản cho thuê, chỉ xem các tài sản để bán hoặc xem tất cả.

Một cách để hoàn thành nhiệm vụ này là kiểm tra loại cho từng MarsProperty trong lưới tổng quan và chỉ hiển thị các thuộc tính phù hợp. Tuy nhiên, dịch vụ web thực tế trên sao Hoả có một tham số hoặc lựa chọn truy vấn (gọi là filter) cho phép bạn chỉ nhận được các tài sản thuộc loại rent hoặc loại buy. Bạn có thể sử dụng truy vấn bộ lọc này với URL dịch vụ web realestate trong một trình duyệt như sau:
https://android-kotlin-fun-mars-server.appspot.com/realestate?filter=buyTrong nhiệm vụ này, bạn sẽ sửa đổi lớp MarsApiService để thêm một lựa chọn truy vấn vào yêu cầu dịch vụ web bằng Retrofit. Sau đó, bạn kết nối trình đơn tuỳ chọn để tải lại tất cả dữ liệu về tài sản trên sao Hoả bằng cách sử dụng tuỳ chọn truy vấn đó. Vì phản hồi mà bạn nhận được từ dịch vụ web chỉ chứa những thuộc tính mà bạn quan tâm, nên bạn không cần thay đổi logic hiển thị của khung hiển thị cho lưới tổng quan.
Bước 1: Cập nhật dịch vụ Mars API
Để thay đổi yêu cầu, bạn cần truy cập lại vào lớp MarsApiService mà bạn đã triển khai trong lớp học lập trình đầu tiên của loạt lớp học lập trình này. Bạn sửa đổi lớp để cung cấp một API lọc.
- Mở
network/MarsApiService.kt. Ngay bên dưới các lệnh nhập, hãy tạo mộtenumcó tên làMarsApiFilterđể xác định các hằng số khớp với các giá trị truy vấn mà dịch vụ web mong đợi.
enum class MarsApiFilter(val value: String) {
SHOW_RENT("rent"),
SHOW_BUY("buy"),
SHOW_ALL("all") }- Sửa đổi phương thức
getProperties()để lấy dữ liệu đầu vào là chuỗi cho truy vấn bộ lọc và chú thích dữ liệu đầu vào đó bằng@Query("filter"), như minh hoạ bên dưới.
Nhậpretrofit2.http.Querykhi được nhắc.
Chú thích@Querycho biết phương thứcgetProperties()(và do đó là Retrofit) sẽ đưa ra yêu cầu dịch vụ web bằng lựa chọn bộ lọc. Mỗi khigetProperties()được gọi, URL yêu cầu sẽ bao gồm phần?filter=type. Phần này hướng dẫn dịch vụ web phản hồi bằng những kết quả phù hợp với truy vấn đó.
fun getProperties(@Query("filter") type: String): Bước 2: Cập nhật mô hình hiển thị tổng quan
Bạn yêu cầu dữ liệu từ MarsApiService trong phương thức getMarsRealEstateProperties() trong OverviewViewModel. Bây giờ, bạn cần cập nhật yêu cầu đó để lấy đối số bộ lọc.
- Mở
overview/OverviewViewModel.kt. Bạn sẽ thấy lỗi trong Android Studio do những thay đổi mà bạn đã thực hiện ở bước trước. ThêmMarsApiFilter(enum của các giá trị bộ lọc có thể có) làm tham số cho lệnh gọigetMarsRealEstateProperties().
Nhậpcom.example.android.marsrealestate.network.MarsApiFilterkhi có yêu cầu.
private fun getMarsRealEstateProperties(filter: MarsApiFilter) {- Sửa đổi lệnh gọi đến
getProperties()trong dịch vụ Retrofit để truyền truy vấn bộ lọc đó dưới dạng một chuỗi.
var getPropertiesDeferred = MarsApi.retrofitService.getProperties(filter.value)- Trong khối
init {}, hãy truyềnMarsApiFilter.SHOW_ALLlàm đối số chogetMarsRealEstateProperties()để hiện tất cả các thuộc tính khi ứng dụng tải lần đầu.
init {
getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL)
}- Ở cuối lớp, hãy thêm một phương thức
updateFilter()nhận đối sốMarsApiFiltervà gọigetMarsRealEstateProperties()bằng đối số đó.
fun updateFilter(filter: MarsApiFilter) {
getMarsRealEstateProperties(filter)
}Bước 3: Kết nối mảnh với trình đơn tuỳ chọn
Bước cuối cùng là kết nối trình đơn mục bổ sung với mảnh để gọi updateFilter() trên mô hình hiển thị khi người dùng chọn một mục trong trình đơn.
- Mở
res/menu/overflow_menu.xml. Ứng dụng MarsRealEstate có một trình đơn tràn hiện tại cung cấp 3 lựa chọn có sẵn: hiện tất cả cơ sở lưu trú, chỉ hiện cơ sở lưu trú cho thuê và chỉ hiện cơ sở lưu trú rao bán.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/show_all_menu"
android:title="@string/show_all" />
<item
android:id="@+id/show_rent_menu"
android:title="@string/show_rent" />
<item
android:id="@+id/show_buy_menu"
android:title="@string/show_buy" />
</menu>- Mở
overview/OverviewFragment.kt. Ở cuối lớp, hãy triển khai phương thứconOptionsItemSelected()để xử lý các lựa chọn về mục trong trình đơn.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
} - Trong
onOptionsItemSelected(), hãy gọi phương thứcupdateFilter()trên mô hình hiển thị bằng bộ lọc thích hợp. Sử dụng khốiwhen {}Kotlin để chuyển đổi giữa các lựa chọn. Sử dụngMarsApiFilter.SHOW_ALLcho giá trị bộ lọc mặc định. Trả vềtruevì bạn đã xử lý mục trong trình đơn. NhậpMarsApiFilter(com.example.android.marsrealestate.network.MarsApiFilter) khi có yêu cầu. Phương thứconOptionsItemSelected()hoàn chỉnh được minh hoạ bên dưới.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
viewModel.updateFilter(
when (item.itemId) {
R.id.show_rent_menu -> MarsApiFilter.SHOW_RENT
R.id.show_buy_menu -> MarsApiFilter.SHOW_BUY
else -> MarsApiFilter.SHOW_ALL
}
)
return true
}- Biên dịch và chạy ứng dụng. Ứng dụng sẽ khởi chạy lưới tổng quan đầu tiên với tất cả các loại tài sản và tài sản đang bán được đánh dấu bằng biểu tượng đô la.
- Chọn Thuê trong trình đơn lựa chọn. Các tài sản tải lại và không có tài sản nào xuất hiện cùng biểu tượng đô la. (Chỉ hiển thị cơ sở lưu trú cho thuê.) Bạn có thể phải đợi vài phút để màn hình làm mới và chỉ hiển thị các thuộc tính đã lọc.
- Chọn Mua trong trình đơn lựa chọn. Các tài sản sẽ tải lại và tất cả đều xuất hiện cùng với biểu tượng đô la. (Chỉ những tài sản đang được bán mới xuất hiện.)
Giờ đây, bạn đã có một lưới biểu tượng có thể di chuyển cho các cơ sở lưu trú trên sao Hoả, nhưng đã đến lúc xem thêm thông tin chi tiết. Trong nhiệm vụ này, bạn sẽ thêm một mảnh chi tiết để hiển thị thông tin chi tiết về một tài sản cụ thể. Đoạn chi tiết sẽ cho thấy một hình ảnh lớn hơn, giá và loại tài sản – cho dù đó là tài sản cho thuê hay để bán.

Mảnh này được khởi chạy khi người dùng nhấn vào một hình ảnh trong lưới tổng quan. Để thực hiện việc này, bạn cần thêm một trình nghe onClick vào các mục lưới RecyclerView, rồi chuyển đến mảnh mới. Bạn điều hướng bằng cách kích hoạt thay đổi LiveData trong ViewModel, như bạn đã làm trong suốt các bài học này. Bạn cũng sử dụng trình bổ trợ Safe Args của thành phần Điều hướng để truyền thông tin MarsProperty đã chọn từ mảnh tổng quan sang mảnh chi tiết.
Bước 1: Tạo mô hình khung hiển thị chi tiết và cập nhật bố cục chi tiết
Tương tự như quy trình bạn đã dùng cho mô hình thành phần hiển thị tổng quan và các mảnh, giờ đây, bạn cần triển khai mô hình thành phần hiển thị và các tệp bố cục cho mảnh chi tiết.
- Mở
detail/DetailViewModel.kt. Tương tự như các tệp Kotlin liên quan đến mạng nằm trong thư mụcnetworkvà các tệp tổng quan nằm trongoverview, thư mụcdetailchứa các tệp liên kết với chế độ xem chi tiết. Lưu ý rằng lớpDetailViewModel(hiện đang trống) lấymarsPropertylàm tham số trong hàm khởi tạo.
class DetailViewModel( marsProperty: MarsProperty,
app: Application) : AndroidViewModel(app) {
}- Trong phần định nghĩa lớp, hãy thêm
LiveDatacho thuộc tính Sao Hoả đã chọn để hiển thị thông tin đó trong chế độ xem chi tiết. Làm theo mẫu thông thường để tạo mộtMutableLiveDatanhằm giữ chínhMarsProperty, sau đó hiển thị một thuộc tínhLiveDatacông khai bất biến.
Nhậpandroidx.lifecycle.LiveDatavà nhậpandroidx.lifecycle.MutableLiveDatakhi có yêu cầu.
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
get() = _selectedProperty- Tạo một khối
init {}và đặt giá trị của thuộc tính Mars đã chọn bằng đối tượngMarsPropertytừ hàm khởi tạo.
init {
_selectedProperty.value = marsProperty
}- Mở
res/layout/fragment_detail.xmlvà xem tệp này ở chế độ xem thiết kế.
Đây là tệp bố cục cho mảnh chi tiết. Thẻ này chứa mộtImageViewcho bức ảnh lớn, mộtTextViewcho loại tài sản (cho thuê hoặc bán) và mộtTextViewcho giá. Xin lưu ý rằng bố cục ràng buộc được bao bọc bằng mộtScrollViewđể bố cục này tự động cuộn nếu khung hiển thị quá lớn so với màn hình, chẳng hạn như khi người dùng xem ở chế độ ngang. - Chuyển đến thẻ Văn bản cho bố cục. Ở đầu bố cục, ngay trước phần tử
<ScrollView>, hãy thêm một phần tử<data>để liên kết mô hình xem chi tiết với bố cục.
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>- Thêm thuộc tính
app:imageUrlvào phần tửImageView. Đặt giá trị này thànhimgSrcUrltừ thuộc tính đã chọn của mô hình hiển thị.
Bộ chuyển đổi liên kết tải hình ảnh bằng Glide cũng sẽ tự động được dùng ở đây, vì bộ chuyển đổi đó theo dõi tất cả các thuộc tínhapp:imageUrl.
app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"Bước 2: Xác định thao tác điều hướng trong mô hình khung hiển thị tổng quan
Khi người dùng nhấn vào một bức ảnh trong mô hình tổng quan, thao tác này sẽ kích hoạt hoạt động điều hướng đến một mảnh cho thấy thông tin chi tiết về mục được nhấp.
- Mở
overview/OverviewViewModel.kt. Thêm thuộc tính_navigateToSelectedPropertyMutableLiveDatavà hiển thị thuộc tính đó bằngLiveDatabất biến.
KhiLiveDatanày thay đổi thành giá trị không rỗng, quá trình điều hướng sẽ được kích hoạt. (Bạn sẽ sớm thêm mã để theo dõi biến này và kích hoạt thao tác điều hướng.)
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
get() = _navigateToSelectedProperty- Ở cuối lớp, hãy thêm một phương thức
displayPropertyDetails()đặt _navigateToSelectedPropertythành thuộc tính Sao Hoả đã chọn.
fun displayPropertyDetails(marsProperty: MarsProperty) {
_navigateToSelectedProperty.value = marsProperty
}- Thêm một phương thức
displayPropertyDetailsComplete()để huỷ giá trị của_navigateToSelectedProperty. Bạn cần có thông tin này để đánh dấu trạng thái điều hướng là hoàn tất và tránh kích hoạt lại quá trình điều hướng khi người dùng quay lại chế độ xem chi tiết.
fun displayPropertyDetailsComplete() {
_navigateToSelectedProperty.value = null
}Bước 3: Thiết lập trình nghe lượt nhấp trong trình kết nối lưới và mảnh
- Mở
overview/PhotoGridAdapter.kt. Ở cuối lớp, hãy tạo một lớpOnClickListenertuỳ chỉnh nhận một lambda có tham sốmarsProperty. Bên trong lớp, hãy xác định một hàmonClick()được đặt thành tham số lambda.
class OnClickListener(val clickListener: (marsProperty:MarsProperty) -> Unit) {
fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty)
}- Di chuyển lên phần định nghĩa lớp cho
PhotoGridAdapterrồi thêm một thuộc tínhOnClickListenerriêng tư vào hàm khởi tạo.
class PhotoGridAdapter( private val onClickListener: OnClickListener ) :
ListAdapter<MarsProperty,
PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {- Bạn có thể nhấp vào ảnh bằng cách thêm
onClickListenervào mục lưới trong phương thứconBindviewHolder(). Xác định trình nghe lượt nhấp ở giữa các lệnh gọi đếngetItem() and bind().
override fun onBindViewHolder(holder: MarsPropertyViewHolder, position: Int) {
val marsProperty = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(marsProperty)
}
holder.bind(marsProperty)
}- Mở
overview/OverviewFragment.kt. Trong phương thứconCreateView(), hãy thay thế dòng khởi tạo thuộc tínhbinding.photosGrid.adapterbằng dòng dưới đây.
Mã này thêm đối tượngPhotoGridAdapter.onClickListenervào hàm khởi tạoPhotoGridAdaptervà gọiviewModel.displayPropertyDetails()bằng đối tượngMarsPropertyđược truyền vào. Thao tác này sẽ kích hoạtLiveDatatrong mô hình hiển thị cho hoạt động điều hướng.
binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
viewModel.displayPropertyDetails(it)
})Bước 4: Sửa đổi biểu đồ điều hướng và tạo MarsProperty có thể phân chia
Khi người dùng nhấn vào một bức ảnh trong lưới tổng quan, ứng dụng sẽ chuyển đến mảnh chi tiết và truyền thông tin chi tiết về tài sản đã chọn trên sao Hoả để khung hiển thị chi tiết có thể hiển thị thông tin đó.

Hiện tại, bạn có một trình nghe lượt nhấp từ PhotoGridAdapter để xử lý thao tác nhấn và một cách để kích hoạt thao tác điều hướng từ mô hình hiển thị. Nhưng bạn chưa có đối tượng MarsProperty nào được truyền đến mảnh chi tiết. Để làm việc đó, bạn có thể dùng Safe Args từ thành phần điều hướng.
- Mở
res/navigation/nav_graph.xml. Nhấp vào thẻ Text (Văn bản) để xem mã XML cho biểu đồ điều hướng. - Bên trong phần tử
<fragment>cho mảnh chi tiết, hãy thêm phần tử<argument>như minh hoạ bên dưới. Đối số này có tên làselectedPropertyvà thuộc kiểuMarsProperty.
<argument
android:name="selectedProperty"
app:argType="com.example.android.marsrealestate.network.MarsProperty"
/>- Biên dịch ứng dụng. Thao tác điều hướng sẽ báo lỗi vì
MarsPropertykhông phân chia được. Giao diệnParcelablecho phép các đối tượng được chuyển đổi tuần tự, nhờ đó, dữ liệu của các đối tượng có thể được truyền giữa các mảnh hoặc hoạt động. Trong trường hợp này, để dữ liệu bên trong đối tượngMarsPropertyđược truyền đến mảnh chi tiết thông qua Safe Args,MarsPropertyphải triển khai giao diệnParcelable. Tin vui là Kotlin cung cấp một lối tắt dễ dàng để triển khai giao diện đó. - Mở
network/MarsProperty.kt. Thêm chú thích@Parcelizevào định nghĩa lớp.
Nhậpkotlinx.android.parcel.Parcelizekhi có yêu cầu.
Chú giải@Parcelizedùng các tiện ích Android của Kotlin để tự động triển khai các phương thức trong giao diệnParcelablecho lớp này. Bạn không cần làm gì thêm!
@Parcelize
data class MarsProperty (- Thay đổi định nghĩa lớp của
MarsPropertyđể mở rộngParcelable.
Nhậpandroid.os.Parcelablekhi được yêu cầu.
Định nghĩa lớpMarsPropertygiờ đây có dạng như sau:
@Parcelize
data class MarsProperty (
val id: String,
@Json(name = "img_src") val imgSrcUrl: String,
val type: String,
val price: Double) : Parcelable {Bước 5: Kết nối các mảnh
Bạn vẫn chưa điều hướng – hoạt động điều hướng thực tế diễn ra trong các mảnh. Trong bước này, bạn sẽ thêm các đoạn mã cuối cùng để triển khai thao tác điều hướng giữa các mảnh tổng quan và chi tiết.
- Mở
overview/OverviewFragment.kt. TrongonCreateView(), bên dưới các dòng khởi chạy phương thức tiếp nối lưới ảnh, hãy thêm các dòng bên dưới để theo dõinavigatedToSelectedPropertytừ mô hình hiển thị tổng quan.
Nhậpandroidx.lifecycle.Observervà nhậpandroidx.navigation.fragment.findNavControllerkhi có yêu cầu.
Trình quan sát kiểm thử xemMarsProperty(ittrong hàm lambda) có phải là giá trị rỗng hay không. Nếu không, trình quan sát sẽ lấy bộ điều khiển điều hướng từ mảnh bằngfindNavController(). GọidisplayPropertyDetailsComplete()để yêu cầu mô hình hiển thị đặt lạiLiveDatavề trạng thái rỗng, nhờ đó bạn sẽ không vô tình kích hoạt lại thao tác điều hướng khi ứng dụng quay lạiOverviewFragment.
viewModel.navigateToSelectedProperty.observe(this, Observer {
if ( null != it ) {
this.findNavController().navigate(
OverviewFragmentDirections.actionShowDetail(it))
viewModel.displayPropertyDetailsComplete()
}
})- Mở
detail/DetailFragment.kt. Thêm dòng này ngay bên dưới lệnh gọi đếnsetLifecycleOwner()trong phương thứconCreateView(). Dòng này lấy đối tượngMarsPropertyđã chọn từ Safe Args.
Lưu ý việc sử dụng toán tử xác nhận không rỗng của Kotlin (!!). Nếu không cóselectedProperty, thì đã xảy ra sự cố nghiêm trọng và bạn thực sự muốn mã này gửi một con trỏ rỗng. (Trong mã phát hành chính thức, bạn nên xử lý lỗi đó theo cách nào đó.)
val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty- Tiếp theo, hãy thêm dòng này để nhận một
DetailViewModelFactorymới. Bạn sẽ dùngDetailViewModelFactoryđể lấy một thực thể củaDetailViewModel. Ứng dụng khởi đầu có một cách triển khaiDetailViewModelFactory, vì vậy, tất cả những gì bạn phải làm ở đây là khởi chạy nó.
val viewModelFactory = DetailViewModelFactory(marsProperty, application)- Cuối cùng, hãy thêm dòng này để lấy
DetailViewModeltừ nhà máy và kết nối tất cả các thành phần.
binding.viewModel = ViewModelProviders.of(
this, viewModelFactory).get(DetailViewModel::class.java)- Biên dịch và chạy ứng dụng, rồi nhấn vào ảnh chụp bất kỳ tài sản nào trên sao Hoả. Mảnh chi tiết sẽ xuất hiện cho thông tin chi tiết của tài sản đó. Nhấn vào nút Quay lại để quay lại trang tổng quan và nhận thấy rằng màn hình chi tiết vẫn còn khá thưa thớt. Bạn sẽ hoàn tất việc thêm dữ liệu về tài sản vào trang chi tiết đó trong nhiệm vụ tiếp theo.
Hiện tại, trang chi tiết chỉ hiển thị ảnh chụp sao Hoả mà bạn thường thấy trên trang tổng quan. Lớp MarsProperty cũng có một loại tài sản (thuê hoặc mua) và giá tài sản. Màn hình chi tiết phải có cả hai giá trị này và sẽ hữu ích nếu các tài sản cho thuê cho biết giá là giá trị mỗi tháng. Bạn sử dụng các phép biến đổi LiveData trong mô hình hiển thị để triển khai cả hai việc đó.
- Mở
res/values/strings.xml. Mã khởi đầu bao gồm các tài nguyên chuỗi (như minh hoạ bên dưới) để giúp bạn tạo các chuỗi cho khung hiển thị chi tiết. Đối với giá, bạn sẽ sử dụng tài nguyêndisplay_price_monthly_rentalhoặc tài nguyêndisplay_price, tuỳ thuộc vào loại tài sản.
<string name="type_rent">Rent</string>
<string name="type_sale">Sale</string>
<string name="display_type">For %s</string>
<string name="display_price_monthly_rental">$%,.0f/month</string>
<string name="display_price">$%,.0f</string>- Mở
detail/DetailViewModel.kt. Ở cuối lớp, hãy thêm đoạn mã dưới đây.
Nhậpandroidx.lifecycle.Transformationsnếu có yêu cầu.
Phép biến đổi này kiểm tra xem tài sản đã chọn có phải là tài sản cho thuê hay không, bằng cách sử dụng cùng một phép kiểm thử trong nhiệm vụ đầu tiên. Nếu tài sản là tài sản cho thuê, thì quá trình chuyển đổi sẽ chọn chuỗi thích hợp trong số các tài nguyên bằng một công tắcwhen {}Kotlin. Cả hai chuỗi này đều cần có một số ở cuối, vì vậy, bạn sẽ nốiproperty.pricesau đó.
val displayPropertyPrice = Transformations.map(selectedProperty) {
app.applicationContext.getString(
when (it.isRental) {
true -> R.string.display_price_monthly_rental
false -> R.string.display_price
}, it.price)
}- Nhập lớp
Rđã tạo để có quyền truy cập vào tài nguyên chuỗi trong dự án.
import com.example.android.marsrealestate.R- Sau quá trình chuyển đổi
displayPropertyPrice, hãy thêm mã như minh hoạ bên dưới. Phép biến đổi này nối nhiều tài nguyên chuỗi, dựa trên việc loại tài sản có phải là tài sản cho thuê hay không.
val displayPropertyType = Transformations.map(selectedProperty) {
app.applicationContext.getString(R.string.display_type,
app.applicationContext.getString(
when (it.isRental) {
true -> R.string.type_rent
false -> R.string.type_sale
}))
}- Mở
res/layout/fragment_detail.xml. Bạn chỉ cần làm thêm một việc nữa, đó là liên kết các chuỗi mới (mà bạn đã tạo bằng các phép biến đổiLiveData) với khung hiển thị chi tiết. Để làm việc đó, bạn đặt giá trị của trường văn bản cho văn bản loại thuộc tính thànhviewModel.displayPropertyTypevà trường văn bản cho văn bản giá trị thànhviewModel.displayPropertyPrice.
<TextView
android:id="@+id/property_type_text"
...
android:text="@{viewModel.displayPropertyType}"
...
tools:text="To Rent" />
<TextView
android:id="@+id/price_value_text"
...
android:text="@{viewModel.displayPropertyPrice}"
...
tools:text="$100,000" />- Biên dịch và chạy ứng dụng. Giờ đây, tất cả dữ liệu về tài sản sẽ xuất hiện trên trang chi tiết, được định dạng một cách gọn gàng.

Dự án Android Studio: MarsRealEstateFinal
Biểu thức liên kết
- Sử dụng biểu thức liên kết trong các tệp bố cục XML để thực hiện các thao tác lập trình đơn giản (chẳng hạn như phép toán hoặc kiểm thử có điều kiện) trên dữ liệu được liên kết.
- Để tham chiếu các lớp trong tệp bố cục, hãy sử dụng thẻ
<import>bên trong thẻ<data>.
Các lựa chọn truy vấn dịch vụ web
- Các yêu cầu đối với dịch vụ web có thể bao gồm các tham số không bắt buộc.
- Để chỉ định các tham số truy vấn trong yêu cầu, hãy sử dụng chú thích
@Querytrong Retrofit.
Khoá học của Udacity:
Tài liệu dành cho nhà phát triển Android:
- Tổng quan về ViewModel
- Tổng quan về LiveData
- Các phương thức điều hợp liên kết (binding adapter)
- Bố cục và biểu thức liên kết
- Điều hướng
- Bắt đầu với thành phần điều hướng
- Truyền dữ liệu giữa các đích đến (cũng mô tả Safe Args)
- Lớp
Transformations - Lớp
ViewModelProvider - Lớp
ViewModelProvider.Factory
Khác:
Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:
- Giao bài tập về nhà nếu cần.
- Trao đổi với học viên về cách nộp bài tập về nhà.
- Chấm điểm bài tập về nhà.
Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.
Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.
Trả lời các câu hỏi sau
Câu hỏi 1
Thẻ <import> trong tệp bố cục XML có chức năng gì?
▢ Thêm một tệp bố cục trong một tệp bố cục khác.
▢ Nhúng mã Kotlin bên trong tệp bố cục.
▢ Cấp quyền truy cập vào các thuộc tính ràng buộc dữ liệu.
▢ Cho phép bạn tham chiếu các lớp và thành phần của lớp trong biểu thức liên kết.
Câu hỏi 2
Bạn có thể làm cách nào để thêm lựa chọn truy vấn vào lệnh gọi dịch vụ web Kiến trúc chuyển trạng thái đại diện (REST) trong Retrofit?
▢ Thêm truy vấn vào cuối URL yêu cầu.
▢ Thêm một tham số cho truy vấn vào hàm tạo yêu cầu và chú giải tham số đó bằng @Query.
▢ Sử dụng lớp Query để tạo yêu cầu.
▢ Sử dụng phương thức addQuery() trong trình tạo Retrofit.
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 khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.