Kiến thức cơ bản về Kotlin trên Android 07.1: Kiến thức cơ bản về RecyclerView

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

Lớp học lập trình này hướng dẫn bạn cách sử dụng RecyclerView để hiển thị danh sách các mục. Dựa trên ứng dụng theo dõi giấc ngủ trong loạt lớp học lập trình trước, bạn sẽ tìm hiểu một cách hiệu quả và linh hoạt hơn để hiển thị dữ liệu bằng cách sử dụng RecyclerView với cấu trúc được đề xuất.

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

Bạn cần thông thạo:

  • Xây dựng giao diện người dùng (UI) cơ bản bằng cách sử dụng một hoạt động, các mảnh và khung hiển thị.
  • Điều hướng giữa các mảnh và sử dụng safeArgs để truyền dữ liệu giữa các mảnh.
  • Sử dụng cá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ũng như các đối tượng theo dõi của chúng.
  • Tạo cơ sở dữ liệu Room, tạo DAO và xác định các thực thể.
  • Sử dụng coroutine cho các tác vụ cơ sở dữ liệu và các tác vụ chạy trong thời gian dài khác.

Kiến thức bạn sẽ học được

  • Cách sử dụng RecyclerView với AdapterViewHolder để hiển thị danh sách các mục.

Bạn sẽ thực hiện

  • Thay đổi ứng dụng TrackMySleepQuality từ bài học trước để sử dụng một RecyclerView nhằm hiển thị dữ liệu về chất lượng giấc ngủ.

Trong lớp học lập trình này, bạn sẽ tạo phần RecyclerView của một ứng dụng theo dõi chất lượng giấc ngủ. Ứng dụng này sử dụng cơ sở dữ liệu Room để lưu trữ dữ liệu giấc ngủ theo thời gian.

Ứng dụng theo dõi giấc ngủ khởi đầu có 2 màn hình, được biểu thị bằng các mảnh, như minh hoạ trong hình bên dưới.

Màn hình đầu tiên (xuất hiện ở bên trái) có các nút để bắt đầu và dừng theo dõi. Màn hình này cũng cho thấy tất cả dữ liệu về giấc ngủ của người dùng. Nút Xoá sẽ xoá vĩnh viễn tất cả dữ liệu mà ứng dụng đã thu thập cho người dùng. Màn hình thứ hai (ở bên phải) là màn hình chọn mức đánh giá chất lượng giấc ngủ.

Ứng dụng này sử dụng một cấu trúc đơn giản hoá với một đơn vị điều khiển giao diện người dùng, ViewModelLiveData. Ứng dụng này cũng sử dụng cơ sở dữ liệu Room để duy trì dữ liệu giấc ngủ.

Danh sách các đêm ngủ xuất hiện trên màn hình đầu tiên có chức năng nhưng không đẹp mắt. Ứng dụng này sử dụng một trình định dạng phức tạp để tạo chuỗi văn bản cho chế độ xem văn bản và các số cho chất lượng. Ngoài ra, thiết kế này không có khả năng mở rộng. Sau khi bạn khắc phục tất cả các vấn đề này trong lớp học lập trình này, ứng dụng cuối cùng sẽ có cùng chức năng và màn hình chính sẽ trông như sau:

Hiển thị danh sách hoặc lưới dữ liệu là một trong những tác vụ giao diện người dùng phổ biến nhất trên Android. Danh sách có thể từ đơn giản đến rất phức tạp. Danh sách khung hiển thị văn bản có thể cho thấy dữ liệu đơn giản, chẳng hạn như danh sách mua sắm. Một danh sách phức tạp, chẳng hạn như danh sách có chú thích về các điểm đến du lịch, có thể cho người dùng thấy nhiều thông tin chi tiết trong một lưới có thể cuộn với các tiêu đề.

Để hỗ trợ tất cả các trường hợp sử dụng này, Android cung cấp tiện ích RecyclerView.

Lợi ích lớn nhất của RecyclerView là rất hiệu quả đối với các danh sách lớn:

  • Theo mặc định, RecyclerView chỉ hoạt động để xử lý hoặc vẽ các mục đang xuất hiện trên màn hình. Ví dụ: nếu danh sách của bạn có 1.000 phần tử nhưng chỉ có 10 phần tử nhìn thấy được, thì RecyclerView chỉ dừng lại ở việc vẽ 10 mục trên màn hình. Khi người dùng cuộn, RecyclerView sẽ tìm ra các mục mới cần xuất hiện trên màn hình và chỉ thực hiện đúng lượng công việc để làm hiển thị những mục đó.
  • Khi một mục cuộn ra khỏi màn hình, các khung hiển thị của mục đó sẽ được tái chế. Điều này có nghĩa là phần tử này sẽ chứa nội dung mới cuộn lên màn hình. Tính năng này của RecyclerView giúp tiết kiệm rất nhiều thời gian xử lý, đồng thời giúp cuộn danh sách một cách mượt mà.
  • Khi một mục thay đổi, thay vì vẽ lại toàn bộ danh sách, RecyclerView có thể cập nhật mục đó. Đây là một bước tiến lớn về hiệu suất khi hiển thị danh sách các mục phức tạp!

Trong chuỗi hiển thị bên dưới, bạn có thể thấy rằng một thành phần hiển thị đã được điền dữ liệu, ABC. Sau khi thành phần hiển thị đó ra khỏi màn hình, RecyclerView sẽ sử dụng lại thành phần đó cho dữ liệu mới, XYZ.

Mẫu bộ chuyển đổi

Nếu từng di chuyển giữa các quốc gia sử dụng ổ cắm điện khác nhau, có lẽ bạn biết cách cắm thiết bị vào ổ cắm bằng cách dùng một đầu giắc cắm chuyển đổi. Bộ chuyển đổi cho phép bạn chuyển đổi một loại giắc cắm sang một loại giắc cắm khác, tức là chuyển đổi một giao diện sang một giao diện khác.

Mẫu bộ chuyển đổi trong kỹ thuật phần mềm giúp một đối tượng hoạt động với một API khác. RecyclerView sử dụng một trình chuyển đổi để chuyển đổi dữ liệu ứng dụng thành một thứ mà RecyclerView có thể hiển thị, mà không thay đổi cách ứng dụng lưu trữ và xử lý dữ liệu. Đối với ứng dụng theo dõi giấc ngủ, bạn sẽ tạo một trình chuyển đổi có khả năng điều chỉnh dữ liệu từ cơ sở dữ liệu Room thành một thứ mà RecyclerView biết cách hiển thị mà không cần thay đổi ViewModel.

Triển khai RecyclerView

Để hiển thị dữ liệu trong một RecyclerView, bạn cần có các phần sau:

  • Dữ liệu cần hiển thị.
  • Một thực thể RecyclerView được xác định trong tệp bố cục của bạn, đóng vai trò là vùng chứa cho các khung hiển thị.
  • Một bố cục cho một mục dữ liệu.
    Nếu tất cả các mục trong danh sách đều giống nhau, bạn có thể sử dụng cùng một bố cục cho tất cả các mục đó, nhưng điều này không bắt buộc. Bố cục mục phải được tạo riêng biệt với bố cục của mảnh, để có thể tạo và điền dữ liệu cho một khung hiển thị mục tại một thời điểm.
  • Trình quản lý bố cục.
    Trình quản lý bố cục xử lý việc sắp xếp (bố cục) các thành phần giao diện người dùng trong một khung hiển thị.
  • Một ngăn chứa thành phần hiển thị.
    Ngăn chứa thành phần hiển thị này mở rộng lớp ViewHolder. Nội dung này chứa thông tin về khung hiển thị để hiển thị một mục trong bố cục của mục đó. Trình lưu giữ thành phần hiển thị (view holder) cũng bổ sung thông tin mà RecyclerView sử dụng để di chuyển các thành phần hiển thị xung quanh màn hình một cách hiệu quả.
  • Một bộ chuyển đổi.
    Bộ chuyển đổi này kết nối dữ liệu của bạn với RecyclerView. Nó điều chỉnh dữ liệu để có thể hiển thị trong một ViewHolder. RecyclerView sử dụng trình chuyển đổi để tìm ra cách hiển thị dữ liệu trên màn hình.

Trong nhiệm vụ này, bạn sẽ thêm một RecyclerView vào tệp bố cục và thiết lập một Adapter để hiển thị dữ liệu về giấc ngủ cho RecyclerView.

Bước 1: Thêm RecyclerView bằng LayoutManager

Trong bước này, bạn sẽ thay thế ScrollView bằng RecyclerView trong tệp fragment_sleep_tracker.xml.

  1. Tải ứng dụng RecyclerViewFundamentals-Starter xuống từ GitHub.
  2. Tạo bản dựng và chạy ứng dụng. Lưu ý cách dữ liệu được hiển thị dưới dạng văn bản đơn giản.
  3. Mở tệp bố cục fragment_sleep_tracker.xml trong thẻ Design (Thiết kế) của Android Studio.
  4. Trong ngăn Component Tree (Cây thành phần), hãy xoá ScrollView. Thao tác này cũng sẽ xoá TextView bên trong ScrollView.
  5. Trong ngăn Palette (Bảng khung hiển thị), hãy di chuyển qua danh sách các loại thành phần ở bên trái để tìm Containers (Vùng chứa), rồi chọn thành phần đó.
  6. Kéo một RecyclerView từ ngăn Palette (Bảng chế độ xem) sang ngăn Component Tree (Cây thành phần). Đặt RecyclerView bên trong ConstraintLayout.

  1. Nếu một hộp thoại mở ra hỏi xem bạn có muốn thêm một phần phụ thuộc hay không, hãy nhấp vào OK để cho phép Android Studio thêm phần phụ thuộc recyclerview vào tệp Gradle của bạn. Quá trình này có thể mất vài giây, sau đó ứng dụng của bạn sẽ đồng bộ hoá.

  1. Mở tệp build.gradle của mô-đun, di chuyển đến cuối và lưu ý đến phần phụ thuộc mới, phần phụ thuộc này sẽ có dạng tương tự như mã dưới đây:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. Chuyển về fragment_sleep_tracker.xml.
  2. Trong thẻ Text (Văn bản), hãy tìm mã RecyclerView như minh hoạ dưới đây:
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. Gán cho RecyclerView id của sleep_list.
android:id="@+id/sleep_list"
  1. Đặt RecyclerView để chiếm phần còn lại của màn hình bên trong ConstraintLayout. Để làm việc này, hãy ràng buộc phần trên cùng của RecyclerView với nút Start (Bắt đầu), phần dưới cùng với nút Clear (Xoá) và mỗi bên với thành phần mẹ. Đặt chiều rộng và chiều cao của bố cục thành 0 dp trong Layout Editor hoặc trong XML bằng mã sau:
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. Thêm một trình quản lý bố cục vào XML RecyclerView. Mỗi RecyclerView cần một trình quản lý bố cục để cho biết cách đặt các mục trong danh sách. Android cung cấp một LinearLayoutManager, theo mặc định sẽ bố trí các mục trong danh sách dọc gồm các hàng có chiều rộng đầy đủ.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. Chuyển sang thẻ Design (Thiết kế) và lưu ý rằng các quy tắc hạn chế đã thêm khiến RecyclerView mở rộng để lấp đầy không gian có sẵn.

Bước 2: Tạo bố cục mục danh sách và trình giữ chế độ xem văn bản

RecyclerView chỉ là một vùng chứa. Trong bước này, bạn sẽ tạo bố cục và cơ sở hạ tầng cho các mục sẽ xuất hiện bên trong RecyclerView.

Để có được RecyclerView hoạt động nhanh nhất có thể, ban đầu bạn sử dụng một mục danh sách đơn giản chỉ hiển thị chất lượng giấc ngủ dưới dạng một con số. Để làm được điều này, bạn cần một ngăn chứa thành phần hiển thị, TextItemViewHolder. Bạn cũng cần một khung hiển thị, một TextView, cho dữ liệu. (Trong một bước sau, bạn sẽ tìm hiểu thêm về trình giữ chế độ xem và cách bố trí tất cả dữ liệu về giấc ngủ.)

  1. Tạo một tệp bố cục có tên là text_item_view.xml. Bạn không cần quan tâm đến việc sử dụng phần tử gốc nào, vì bạn sẽ thay thế mã mẫu.
  2. Trong text_item_view.xml, hãy xoá tất cả mã đã cho.
  3. Thêm một TextView có khoảng đệm 16dp ở đầu và cuối, đồng thời có kích thước văn bản là 24sp. Đặt chiều rộng khớp với phần tử mẹ và chiều cao bao bọc nội dung. Vì khung hiển thị này xuất hiện bên trong RecyclerView, nên bạn không cần đặt khung hiển thị này bên trong ViewGroup.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. Mở Util.kt. Di chuyển xuống cuối và thêm định nghĩa xuất hiện bên dưới để tạo lớp TextItemViewHolder. Đặt mã ở cuối tệp, sau dấu ngoặc nhọn đóng cuối cùng. Mã này sẽ nằm trong Util.kt vì trình giữ chế độ xem này chỉ là tạm thời và bạn sẽ thay thế nó sau.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. Nếu được nhắc, hãy nhập android.widget.TextViewandroidx.recyclerview.widget.RecyclerView.

Bước 3: Tạo SleepNightAdapter

Nhiệm vụ cốt lõi khi triển khai RecyclerView là tạo bộ chuyển đổi. Bạn có một phần tử giữ khung hiển thị đơn giản cho khung hiển thị mục và một bố cục cho từng mục. Giờ đây, bạn có thể tạo một trình chuyển đổi. Trình chuyển đổi tạo một ngăn chứa thành phần hiển thị và điền dữ liệu vào đó để RecyclerView hiển thị.

  1. Trong gói sleeptracker, hãy tạo một lớp Kotlin mới có tên là SleepNightAdapter.
  2. Mở rộng lớp SleepNightAdapter thành RecyclerView.Adapter. Lớp này có tên là SleepNightAdapter vì nó điều chỉnh một đối tượng SleepNight thành một đối tượng mà RecyclerView có thể sử dụng. Bộ chuyển đổi cần biết nên sử dụng trình giữ khung hiển thị nào, vì vậy, hãy truyền TextItemViewHolder vào. Nhập các thành phần cần thiết khi được nhắc, sau đó bạn sẽ thấy lỗi vì có các phương thức bắt buộc cần triển khai.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. Ở cấp cao nhất của SleepNightAdapter, hãy tạo một biến listOf SleepNight để lưu giữ dữ liệu.
var data =  listOf<SleepNight>()
  1. Trong SleepNightAdapter, hãy ghi đè getItemCount() để trả về kích thước của danh sách các đêm ngủ trong data. RecyclerView cần biết số lượng mục mà bộ chuyển đổi có để hiển thị và thực hiện việc đó bằng cách gọi getItemCount().
override fun getItemCount() = data.size
  1. Trong SleepNightAdapter, hãy ghi đè hàm onBindViewHolder(), như minh hoạ dưới đây.

    Hàm onBindViewHolder() được RecyclerView gọi để hiển thị dữ liệu cho một mục trong danh sách ở vị trí đã chỉ định. Vì vậy, phương thức onBindViewHolder() sẽ nhận hai đối số: một trình giữ khung hiển thị và một vị trí của dữ liệu cần liên kết. Đối với ứng dụng này, phần giữ chỗ là TextItemViewHolder và vị trí là vị trí trong danh sách.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. Bên trong onBindViewHolder(), hãy tạo một biến cho một mục ở một vị trí nhất định trong dữ liệu.
 val item = data[position]
  1. ViewHolder mà bạn đã tạo có một thuộc tính tên là textView. Trong onBindViewHolder(), hãy đặt text của textView thành số chất lượng giấc ngủ. Đoạn mã này chỉ hiển thị một danh sách các số, nhưng ví dụ đơn giản này cho phép bạn xem cách bộ chuyển đổi đưa dữ liệu vào trình giữ khung hiển thị và lên màn hình.
holder.textView.text = item.sleepQuality.toString()
  1. Trong SleepNightAdapter, hãy ghi đè và triển khai onCreateViewHolder(). Phương thức này được gọi khi RecyclerView cần một thành phần chứa chế độ xem để biểu thị một mục.

    Hàm này nhận 2 tham số và trả về một ViewHolder. Tham số parent (nhóm thành phần hiển thị chứa ngăn chứa thành phần hiển thị) luôn là RecyclerView. Tham số viewType được dùng khi có nhiều khung hiển thị trong cùng một RecyclerView. Ví dụ: nếu bạn đặt một danh sách các khung hiển thị văn bản, một hình ảnh và một video vào cùng một RecyclerView, thì hàm onCreateViewHolder() sẽ cần biết loại khung hiển thị cần dùng.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. Trong onCreateViewHolder(), hãy tạo một thực thể của LayoutInflater.

    Trình tăng cường bố cục biết cách tạo khung hiển thị từ bố cục XML. context chứa thông tin về cách tăng kích thước khung hiển thị một cách chính xác. Trong một bộ chuyển đổi cho thành phần hiển thị tái chế, bạn luôn truyền vào ngữ cảnh của nhóm thành phần hiển thị parent, đó là RecyclerView.
val layoutInflater = LayoutInflater.from(parent.context)
  1. Trong onCreateViewHolder(), hãy tạo view bằng cách yêu cầu layoutinflater mở rộng view.

    Truyền bố cục XML cho khung hiển thị và nhóm khung hiển thị parent cho khung hiển thị. Đối số thứ ba (boolean) là attachToRoot. Đối số này cần mang giá trị false bởi vì RecyclerView sẽ thêm phần tử này vào hệ phân cấp thành phần hiển thị vào thời điểm thích hợp.
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. Trong onCreateViewHolder(), hãy trả về một TextItemViewHolder được tạo bằng view.
return TextItemViewHolder(view)
  1. Trình chuyển đổi cần cho RecyclerView biết khi data thay đổi, vì RecyclerView không biết gì về dữ liệu. Nó chỉ biết về các trình giữ khung hiển thị mà bộ điều hợp cung cấp cho nó.

    Để cho RecyclerView biết khi nào dữ liệu mà nó đang hiển thị đã thay đổi, hãy thêm một phương thức thiết lập tuỳ chỉnh vào biến data ở đầu lớp SleepNightAdapter. Trong phương thức thiết lập, hãy gán cho data một giá trị mới, sau đó gọi notifyDataSetChanged() để kích hoạt việc vẽ lại danh sách bằng dữ liệu mới.
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

Bước 4: Cho RecyclerView biết về Bộ chuyển đổi

RecyclerView cần biết về bộ chuyển đổi để sử dụng nhằm lấy trình giữ khung hiển thị.

  1. Mở SleepTrackerFragment.kt.
  2. Trong onCreateview(), hãy tạo một bộ chuyển đổi. Đặt mã này sau khi tạo mô hình ViewModel và trước câu lệnh return.
val adapter = SleepNightAdapter()
  1. Liên kết adapter với RecyclerView.
binding.sleepList.adapter = adapter
  1. Dọn dẹp và tạo lại dự án để cập nhật đối tượng binding.

    Nếu bạn vẫn thấy lỗi liên quan đến binding.sleepList hoặc binding.FragmentSleepTrackerBinding, hãy vô hiệu hoá bộ nhớ đệm rồi khởi động lại. (Chọn File > Invalidate Caches / Restart (Tệp > Huỷ bộ nhớ đệm/Khởi động lại).)

    Nếu chạy ứng dụng ngay bây giờ, bạn sẽ không thấy lỗi nào, nhưng sẽ không thấy dữ liệu nào hiển thị khi nhấn vào Start (Bắt đầu), rồi nhấn vào Stop (Dừng).

Bước 5: Nhập dữ liệu vào bộ chuyển đổi

Đến đây, bạn đã có một trình chuyển đổi và cách lấy dữ liệu từ trình chuyển đổi vào RecyclerView. Bây giờ, bạn cần lấy dữ liệu vào bộ chuyển đổi từ ViewModel.

  1. Mở SleepTrackerViewModel.
  2. Tìm biến nights. Biến này lưu trữ tất cả các đêm ngủ, đây là dữ liệu cần hiển thị. Biến nights được đặt bằng cách gọi getAllNights() trên cơ sở dữ liệu.
  3. Xoá private khỏi nights, vì bạn sẽ tạo một đối tượng tiếp nhận dữ liệu cần truy cập vào biến này. Nội dung khai báo của bạn sẽ có dạng như sau:
val nights = database.getAllNights()
  1. Trong gói database, hãy mở SleepDatabaseDao.
  2. Tìm hàm getAllNights(). Xin lưu ý rằng hàm này trả về danh sách các giá trị SleepNight dưới dạng LiveData. Điều này có nghĩa là biến nights chứa LiveData được Room cập nhật liên tục và bạn có thể quan sát nights để biết thời điểm biến này thay đổi.
  3. Mở SleepTrackerFragment.
  4. Trong onCreateView(), bên dưới quá trình tạo adapter, hãy tạo một trình quan sát trên biến nights.

    Bằng cách cung cấp viewLifecycleOwner của mảnh dưới dạng chủ sở hữu vòng đời, bạn có thể đảm bảo rằng đối tượng tiếp nhận dữ liệu này chỉ hoạt động khi RecyclerView ở trên màn hình.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. Bên trong đối tượng theo dõi, bất cứ khi nào bạn nhận được một giá trị không rỗng (đối với nights), hãy chỉ định giá trị đó cho data của bộ chuyển đổi. Đây là mã hoàn chỉnh cho đối tượng theo dõi và việc thiết lập dữ liệu:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. Tạo và chạy mã của bạn.

    Bạn sẽ thấy danh sách các chỉ số về chất lượng giấc ngủ nếu bộ chuyển đổi đang hoạt động. Ảnh chụp màn hình bên trái cho thấy số -1 sau khi bạn nhấn vào Bắt đầu. Ảnh chụp màn hình bên phải cho thấy số liệu mới về chất lượng giấc ngủ sau khi bạn nhấn vào Dừng và chọn một điểm xếp hạng chất lượng.

Bước 6: Khám phá cách các trình giữ khung hiển thị được tái chế

RecyclerView tái chế các lớp lưu giữ thành phần hiển thị, tức là sử dụng lại các lớp này. Khi một thành phần hiển thị cuộn ra khỏi màn hình, RecyclerView sẽ sử dụng lại thành phần hiển thị đó cho thành phần hiển thị sắp cuộn vào màn hình.

Vì các trình giữ khung hiển thị này được tái chế, hãy đảm bảo onBindViewHolder() đặt hoặc đặt lại mọi chế độ tuỳ chỉnh mà các mục trước đó có thể đã đặt trên một trình giữ khung hiển thị.

Ví dụ: bạn có thể đặt màu văn bản thành màu đỏ trong các trình giữ khung hiển thị chứa điểm xếp hạng chất lượng nhỏ hơn hoặc bằng 1 và thể hiện giấc ngủ kém.

  1. Trong lớp SleepNightAdapter, hãy thêm đoạn mã sau vào cuối onBindViewHolder().
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. Chạy ứng dụng.
  2. Thêm một số dữ liệu về giấc ngủ kém chất lượng và số liệu này sẽ có màu đỏ.
  3. Thêm điểm đánh giá cao cho chất lượng giấc ngủ cho đến khi bạn thấy một số màu đỏ trên màn hình.

    RecyclerView sử dụng lại các trình giữ khung hiển thị, nên cuối cùng, nó sẽ sử dụng lại một trong các trình giữ khung hiển thị màu đỏ cho điểm đánh giá chất lượng cao. Đánh giá cao bị hiển thị nhầm bằng màu đỏ.

  1. Để khắc phục vấn đề này, hãy thêm một câu lệnh else để đặt màu thành đen nếu chất lượng không nhỏ hơn hoặc bằng một.

    Với cả hai điều kiện rõ ràng, trình giữ khung hiển thị sẽ sử dụng đúng màu văn bản cho từng mục.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. Chạy ứng dụng và các số phải luôn có màu chính xác.

Xin chúc mừng! Giờ đây, bạn đã có một RecyclerView cơ bản hoạt động đầy đủ.

Trong nhiệm vụ này, bạn sẽ thay thế trình giữ khung hiển thị đơn giản bằng một trình giữ khung hiển thị có thể hiển thị nhiều dữ liệu hơn cho một đêm ngủ.

ViewHolder đơn giản mà bạn đã thêm vào Util.kt chỉ bao bọc một TextView trong một TextItemViewHolder.

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

Vậy tại sao RecyclerView không chỉ sử dụng trực tiếp TextView? Một dòng mã này cung cấp rất nhiều chức năng. ViewHolder mô tả một khung hiển thị mục và siêu dữ liệu về vị trí của khung hiển thị đó trong RecyclerView. RecyclerView dựa vào chức năng này để định vị chính xác khung hiển thị khi danh sách cuộn và thực hiện những việc thú vị như tạo hiệu ứng cho khung hiển thị khi các mục được thêm hoặc xoá trong Adapter.

Nếu RecyclerView cần truy cập vào các khung hiển thị được lưu trữ trong ViewHolder, thì nó có thể thực hiện việc này bằng cách sử dụng thuộc tính itemView của trình giữ khung hiển thị. RecyclerView sử dụng itemView khi liên kết một mục để hiển thị trên màn hình, khi vẽ các thành phần trang trí xung quanh một khung hiển thị như đường viền và để triển khai khả năng tiếp cận.

Bước 1: Tạo bố cục mục

Trong bước này, bạn sẽ tạo tệp bố cục cho một mục. Bố cục này bao gồm một ConstraintLayoutImageView cho chất lượng giấc ngủ, TextView cho thời lượng ngủ và TextView cho chất lượng dưới dạng văn bản. Vì bạn đã từng tạo bố cục trước đây, hãy sao chép và dán mã XML được cung cấp.

  1. Tạo một tệp tài nguyên bố cục mới và đặt tên là list_item_sleep_night.
  2. Thay thế toàn bộ mã trong tệp bằng mã bên dưới. Sau đó, hãy làm quen với bố cục bạn vừa tạo.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Chuyển sang thẻ Thiết kế trong Android Studio. Trong chế độ xem thiết kế, bố cục của bạn sẽ trông giống như ảnh chụp màn hình ở bên trái bên dưới. Ở chế độ xem bản thiết kế, giao diện sẽ giống như ảnh chụp màn hình ở bên phải.

Bước 2: Tạo ViewHolder

  1. Mở SleepNightAdapter.kt.
  2. Tạo một lớp bên trong SleepNightAdapter có tên là ViewHolder và mở rộng RecyclerView.ViewHolder.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. Bên trong ViewHolder, hãy tham chiếu đến các thành phần hiển thị. Bạn cần có một tham chiếu đến các khung hiển thị mà ViewHolder này sẽ cập nhật. Mỗi khi liên kết ViewHolder này, bạn cần truy cập vào hình ảnh và cả hai khung hiển thị văn bản. (Sau này, bạn sẽ chuyển đổi mã này để sử dụng tính năng liên kết dữ liệu.)
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

Bước 3: Dùng ViewHolder trong SleepNightAdapter

  1. Trong định nghĩa SleepNightAdapter, thay vì TextItemViewHolder, hãy dùng SleepNightAdapter.ViewHolder mà bạn vừa tạo.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

Cập nhật onCreateViewHolder():

  1. Thay đổi chữ ký của onCreateViewHolder() để trả về ViewHolder.
  2. Thay đổi trình mở rộng bố cục để sử dụng tài nguyên bố cục chính xác, list_item_sleep_night.
  3. Xoá thao tác truyền đến TextView.
  4. Thay vì trả về TextItemViewHolder, hãy trả về ViewHolder.

    Sau đây là hàm onCreateViewHolder() đã cập nhật hoàn tất:
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

Cập nhật onBindViewHolder():

  1. Thay đổi chữ ký của onBindViewHolder() để tham số holderViewHolder thay vì TextItemViewHolder.
  2. Trong onBindViewHolder(), hãy xoá toàn bộ mã, ngoại trừ định nghĩa của item.
  3. Xác định một val res chứa một tham chiếu đến resources cho khung hiển thị này.
val res = holder.itemView.context.resources
  1. Đặt văn bản của khung hiển thị văn bản sleepLength thành thời lượng. Sao chép mã bên dưới. Mã này gọi một hàm định dạng được cung cấp cùng với mã khởi đầu.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. Điều này sẽ gây ra lỗi vì bạn cần xác định convertDurationToFormatted(). Mở Util.kt rồi bỏ chú thích mã và các nội dung nhập được liên kết cho mã đó. (Chọn Code > Comment with Line comments (Mã > Thêm chú thích bằng chú thích dòng).)
  2. Quay lại onBindViewHolder(), hãy dùng convertNumericQualityToString() để đặt chất lượng.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. Bạn có thể cần nhập các hàm này theo cách thủ công.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. Đặt biểu tượng chính xác cho chất lượng. Biểu tượng ic_sleep_active mới được cung cấp cho bạn trong mã khởi đầu.
holder.qualityImage.setImageResource(when (item.sleepQuality) {
   0 -> R.drawable.ic_sleep_0
   1 -> R.drawable.ic_sleep_1
   2 -> R.drawable.ic_sleep_2
   3 -> R.drawable.ic_sleep_3
   4 -> R.drawable.ic_sleep_4
   5 -> R.drawable.ic_sleep_5
   else -> R.drawable.ic_sleep_active
})
  1. Sau đây là hàm onBindViewHolder() đã cập nhật hoàn tất, thiết lập tất cả dữ liệu cho ViewHolder:
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. Chạy ứng dụng. Màn hình sẽ có dạng như ảnh chụp màn hình dưới đây, cho thấy biểu tượng chất lượng giấc ngủ, cùng với văn bản về thời lượng và chất lượng giấc ngủ.

Bạn hiện đã hoàn tất thành phần kết hợp RecyclerView! Bạn đã tìm hiểu cách triển khai AdapterViewHolder, đồng thời kết hợp chúng để hiển thị một danh sách bằng RecyclerView Adapter.

Đoạn mã của bạn cho đến nay cho thấy quy trình tạo một bộ chuyển đổi và ngăn chứa thành phần hiển thị. Tuy nhiên, bạn có thể cải thiện mã này. Mã để hiển thị và mã để quản lý trình lưu giữ thành phần hiển thị bị lẫn lộn, đồng thời onBindViewHolder() biết thông tin chi tiết về cách cập nhật ViewHolder.

Trong một ứng dụng phát hành công khai, bạn có thể có nhiều trình giữ chế độ xem, bộ điều hợp phức tạp hơn và nhiều nhà phát triển thực hiện các thay đổi. Bạn nên cấu trúc mã sao cho mọi thứ liên quan đến một trình giữ khung hiển thị chỉ nằm trong trình giữ khung hiển thị.

Bước 1: Tái cấu trúc onBindViewHolder()

Trong bước này, bạn sẽ tái cấu trúc mã và di chuyển tất cả chức năng của trình giữ chế độ xem vào ViewHolder. Mục đích của việc tái cấu trúc này không phải là thay đổi giao diện của ứng dụng đối với người dùng, mà là giúp nhà phát triển dễ dàng và an toàn hơn khi làm việc trên mã. May mắn là Android Studio có các công cụ hỗ trợ.

  1. Trong SleepNightAdapter, trong onBindViewHolder(), hãy chọn mọi thứ ngoại trừ câu lệnh khai báo biến item.
  2. Nhấp chuột phải, rồi chọn Refactor > Extract > Function (Tái cấu trúc > Trích xuất > Hàm).
  3. Đặt tên cho hàm là bind và chấp nhận các tham số được đề xuất. Nhấp vào OK.

    Hàm bind() được đặt bên dưới onBindViewHolder().
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. Đặt con trỏ lên từ holder của tham số holder trong bind(). Nhấn Alt+Enter (Option+Enter trên máy Mac) để mở trình đơn ý định. Chọn Convert parameter to receiver (Chuyển đổi tham số thành đối tượng nhận) để chuyển đổi tham số này thành một hàm mở rộng có chữ ký sau:
private fun ViewHolder.bind(item: SleepNight) {...}
  1. Cắt và dán hàm bind() vào ViewHolder.
  2. Đặt bind() ở chế độ công khai.
  3. Nhập bind() vào bộ chuyển đổi, nếu cần.
  4. Vì khoá này hiện nằm trong ViewHolder, nên bạn có thể xoá phần ViewHolder của chữ ký. Sau đây là mã hoàn chỉnh cho hàm bind() trong lớp ViewHolder.
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

Bước 2: Tái cấu trúc onCreateViewHolder

Phương thức onCreateViewHolder() trong trình chuyển đổi hiện tăng cường thành phần hiển thị từ tài nguyên bố cục cho ViewHolder. Tuy nhiên, lạm phát không liên quan gì đến bộ chuyển đổi mà liên quan đến ViewHolder. Lạm phát sẽ xảy ra trong ViewHolder.

  1. Trong onCreateViewHolder(), hãy chọn tất cả mã trong phần nội dung của hàm.
  2. Nhấp chuột phải, rồi chọn Refactor > Extract > Function (Tái cấu trúc > Trích xuất > Hàm).
  3. Đặt tên cho hàm là from và chấp nhận các tham số được đề xuất. Nhấp vào OK.
  4. Đặt con trỏ lên tên hàm from. Nhấn Alt+Enter (Option+Enter trên máy Mac) để mở trình đơn ý định.
  5. Chọn Move to companion object (Di chuyển đến đối tượng đồng hành). Hàm from() cần nằm trong một đối tượng companion để có thể được gọi trên lớp ViewHolder, chứ không được gọi trên một thực thể ViewHolder.
  6. Di chuyển đối tượng companion vào lớp ViewHolder.
  7. Đặt from() ở chế độ công khai.
  8. Trong onCreateViewHolder(), hãy thay đổi câu lệnh return để trả về kết quả của việc gọi from() trong lớp ViewHolder.

    Phương thức onCreateViewHolder()from() đã hoàn tất của bạn sẽ có dạng như mã bên dưới, đồng thời mã của bạn sẽ được tạo và chạy mà không gặp lỗi.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. Thay đổi chữ ký của lớp ViewHolder để hàm khởi tạo là private. Vì from() hiện là một phương thức trả về một thực thể ViewHolder mới, nên không có lý do gì để người dùng gọi hàm khởi tạo của ViewHolder nữa.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. Chạy ứng dụng. Ứng dụng của bạn sẽ tạo và chạy giống như trước đây. Đây là kết quả mong muốn sau khi tái cấu trúc.

Dự án Android Studio: RecyclerViewFundamentals

  • Hiển thị danh sách hoặc lưới dữ liệu là một trong những tác vụ giao diện người dùng phổ biến nhất trên Android. RecyclerView được thiết kế để hoạt động hiệu quả ngay cả khi hiển thị danh sách có nhiều mục.
  • RecyclerView chỉ thực hiện những việc cần thiết để xử lý hoặc vẽ các mục đang xuất hiện trên màn hình.
  • Khi một mục cuộn ra khỏi màn hình, các khung hiển thị của mục đó sẽ được tái chế. Điều này có nghĩa là phần tử này sẽ chứa nội dung mới cuộn lên màn hình.
  • Mẫu bộ chuyển đổi trong kỹ thuật phần mềm giúp một đối tượng hoạt động cùng với một API khác. RecyclerView sử dụng một trình chuyển đổi để chuyển đổi dữ liệu ứng dụng thành một thứ mà nó có thể hiển thị, mà không cần thay đổi cách ứng dụng lưu trữ và xử lý dữ liệu.

Để hiển thị dữ liệu trong một RecyclerView, bạn cần có các phần sau:

  • RecyclerView
    Để tạo một thực thể của RecyclerView, hãy xác định một phần tử <RecyclerView> trong tệp bố cục.
  • LayoutManager
    RecyclerView sử dụng LayoutManager để sắp xếp bố cục của các mục trong RecyclerView, chẳng hạn như sắp xếp các mục đó trong một lưới hoặc trong một danh sách tuyến tính.

    Trong <RecyclerView> trong tệp bố cục, hãy đặt thuộc tính app:layoutManager thành trình quản lý bố cục (chẳng hạn như LinearLayoutManager hoặc GridLayoutManager).

    Bạn cũng có thể đặt LayoutManager cho RecyclerView theo phương thức lập trình. (Kỹ thuật này sẽ được đề cập trong một lớp học lập trình sau này.)
  • Bố cục cho từng mục
    Tạo bố cục cho một mục dữ liệu trong tệp bố cục XML.
  • Trình chuyển đổi
    Tạo một trình chuyển đổi để chuẩn bị dữ liệu và cách dữ liệu sẽ xuất hiện trong ViewHolder. Liên kết đối tượng chuyển đổi với RecyclerView.

    Khi RecyclerView chạy, nó sẽ sử dụng trình chuyển đổi để tìm ra cách hiển thị dữ liệu trên màn hình.

    Trình chuyển đổi yêu cầu bạn triển khai các phương thức sau:
    getItemCount() để trả về số lượng mục.
    onCreateViewHolder() để trả về ViewHolder cho một mục trong danh sách.
    onBindViewHolder() để điều chỉnh dữ liệu cho các khung hiển thị của một mục trong danh sách.

  • ViewHolder
    ViewHolder chứa thông tin về chế độ xem để hiển thị một mục trong bố cục của mục đó.
  • Phương thức onBindViewHolder() trong trình chuyển đổi sẽ điều chỉnh dữ liệu cho phù hợp với các thành phần hiển thị. Bạn luôn ghi đè phương thức này. Thông thường, onBindViewHolder() sẽ mở rộng bố cục cho một mục và đặt dữ liệu vào các thành phần hiển thị trong bố cục.
  • RecyclerView không biết gì về dữ liệu, nên Adapter cần thông báo cho RecyclerView khi dữ liệu đó thay đổi. Sử dụng notifyDataSetChanged() để thông báo cho Adapter rằng dữ liệu đã thay đổi.

Khoá học của Udacity:

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

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

RecyclerView hiển thị các mục như thế nào? Hãy chọn mọi câu trả lời phù hợp.

▢ Hiển thị các mục trong một danh sách hoặc một lưới.

▢ Cuộn theo chiều dọc hoặc chiều ngang.

▢ Cuộn theo đường chéo trên các thiết bị lớn hơn, chẳng hạn như máy tính bảng.

▢ Cho phép bố cục tuỳ chỉnh khi một danh sách hoặc một lưới không đủ cho trường hợp sử dụng.

Câu hỏi 2

Lợi ích của việc dùng RecyclerView là gì? Hãy chọn mọi câu trả lời phù hợp.

▢ Hiển thị danh sách có nhiều mục một cách hiệu quả.

▢ Tự động cập nhật dữ liệu.

▢ Giảm thiểu nhu cầu làm mới khi cập nhật, xoá hoặc thêm một mục vào danh sách.

▢ Dùng lại khung hiển thị cuộn khỏi màn hình để hiển thị mục tiếp theo cuộn trên màn hình.

Câu hỏi 3

Một số lý do dùng bộ chuyển đổi là gì? Hãy chọn mọi câu trả lời phù hợp.

▢ Việc tách riêng các vấn đề giúp thay đổi và kiểm thử mã dễ dàng hơn.

RecyclerView độc lập với dữ liệu đang hiển thị.

▢ Các tầng xử lý dữ liệu không cần quan tâm đến cách hiển thị dữ liệu.

▢ Ứng dụng sẽ chạy nhanh hơn.

Câu hỏi 4

Câu nào sau đây đúng về ViewHolder? Hãy chọn mọi câu trả lời phù hợp.

▢ Bố cục ViewHolder được xác định trong các tệp bố cục XML.

▢ Có một ViewHolder cho mỗi đơn vị dữ liệu trong tập dữ liệu.

▢ Bạn có thể có nhiều ViewHolder trong một RecyclerView.

Adapter liên kết dữ liệu với ViewHolder.

Bắt đầu bài học tiếp theo: 7.2: DiffUtil và liên kết dữ liệu bằng RecyclerView