+2

[UTH] Đào sâu về cách hoạt động của RecyclerView

Là một Android Developer thì ai trong chúng ta cũng đều quá quen với RecyclerView, cách sử dụng và các khái niệm xung quanh nó rồi. Vì thế nên câu hỏi đặt ra ở đây là:

Thật sự thì sâu bên trong, RecyclerView hoạt động như thế nào? Tại sao nó lại hoạt động mượt như vậy? Data được sử dụng và hiển thị ra làm sao? Vì sao lại có tên là Recycler(View)?

Image reference: Microsoft docs

Vậy tại sao lại cần biết và hiểu được các yếu tố trên?

Đầu tiên, RecyclerView gần như xuất hiện hầu hết trong mọi ứng dụng android hiện đại ngày nay. Vì thế cách mà chúng ta, các developer làm việc với nó ảnh hưởng rất lớn tới trải nghiệm của hàng triệu triệu người dùng. Có thể dễ dàng tìm thấy hàng loạt các bài viết, video tutorial về việc làm sao để hiển thị data dưới dạng list bằng RecyclerView trong 1 ứng dụng Android. Nhưng, liệu cách tiếp cận trên bề mặt như vậy đã đủ để chúng ta ứng phó với các trường hợp phức tạp như việc có hàng triệu thiết bị Android với các kiểu render UI khác nhau và hàng loạt các vấn đề liên quan tới hiệu năng ứng dụng?

Và đã là 1 developer thì chúng ta không hề muốn người dùng của mình có một trải nghiệm sử dụng tệ(scroll thì giật lag chẳng hạn) ngay trên chính màn hình Home(hoặc bất kỳ màn hình nào) đang hiển thị 1 list các sản phẩm được recommend tới họ. Vì thế, khi biết được chính xác cách thiết kế và hoạt động của RecyclerView, chúng ta sẽ biết cách "adapt" khi gặp các vấn đề liên quan tới nó.

” Tell me and I forget, teach me and I may remember, involve me and I learn.”


Nhưng trước khi tới với cách hoạt động của RecyclerView, đầu tiên chúng ta phải hiểu tại sao nó lại được sinh ra khi đã có ListView.

Có một số vấn đề với ListView như sau:

  • Giật lag khi scroll: ListView khởi tạo số lượng view bằng với số lượng data mà nó nhận vào. Việc khởi tạo view như vậy cực kỳ tốn hiệu năng(nếu số lượng data lớn). Cho dù ta có thể cải thiện bằng cách tự viết logic riêng.
  • Không hỗ trợ animation mặc định: ListView không hề hỗ trợ animation cho các item bên trong nó. Và việc tự tay implement 1 animation hoàn hảo rất tốn kém về mặt thời gian.
  • Chỉ cho phép scroll dọc: Không scroll ngang. ListView chỉ hỗ trợ scroll dọc

Và hàng loạt các lý do khác ngoài những lý do kể trên khiến cho các Android Dev phải thực sự cần tới một thứ gì đó Mới hơn, thứ gì đó có khả năng tối ưu hoá và khả năng chịu tải cao.


RecyclerView - Là cái gì?

Trích theo doc của Android:

RecyclerView is a UI component which allows us to create a scrolling list. It is basically a new ViewGroup used to render any adapter-based view in horizontal/vertical/grid or staggered grid manner using the Viewholder pattern.

Quá nhiều khái niệm? Quá rắc rối? Hãy từ từ, chúng ta sẽ đi qua từng bước một:

Image reference: Google

Có tổng cộng 4 thành phần chính trong RecyclerView:

  • RecyclerView.Adapter: Cho phép bind dữ liệu(dữ liệu cụ thể của ứng dụng đó) vào một view con được hiển thị bên trong RecyclerView (có thể là text, ảnh,...). Adapter sẽ tự biết cách bind dữ liệu cho từng view con sao tương ứng với vị trí của dữ liệu trong bộ dữ liệu được cung cấp
  • RecyclerView.LayoutManager: Sắp xếp các item bên trong RecyclerView. Có thể sử dụng các LayoutManager đã được định nghĩa sẵn (LinearLayoutManager, GridLayourManager,...) hoặc tự implement một LayoutManager hoàn toàn custom tuỳ theo yêu cầu của ứng dụng.
  • RecyclerView.ItemAnimator: RecyclerView cung cấp một số các animation mặc định mà chúng ta có thể override và thay đổi tuỳ theo nhu cầu. Mặc định, RecyclerView sử dụng DefaultItemAnimator.
  • RecyclerView.ViewHolder: Thành phần này là bắt buộc nếu chúng ta muốn sử dụng RecyclerView. ViewHolder sẽ thực hiện việc vẽ các thành phần UI cho từng item lên màn hình.

Để dễ hiểu và tiếp cận các thành phần trên thì chúng ta sẽ có 1 ví dụ,

Giả sử, có 1 hàng dài người ta đang xếp hàng trước quầy bán gà rán, nhưng chỉ có 5 người được phục vụ tại 1 thời điểm. Người phục vụ phát cho 5 người đó mỗi người 1 cái đĩa sạch và trữ sẵn 1 lượng nhỏ đĩa ăn sạch để phát cho những người xếp hàng ở sau 5 người đó. Những người đã ăn xong chuyển cái đĩa bẩn lại cho người phục vụ, anh ta nhanh chóng chùi rửa sạch cái đĩa và sẵn sàng phát nó cho những người tiếp theo.

Image reference: Google

Ở đây, người phục vụ là Recycler, những cái đĩa là các View, và miếng gà rán là Data. Tại 1 thời điểm chỉ có 5 người được phục vụ(visible trên màn hình) và ý tưởng chỉ sử dụng một lượng nhỏ số lượng đĩa chính là ViewHolder khi mà các cái đĩa đó được chùi rửa sạch và được phát lại cho những người đứng sau. Đây chính là lợi thế lớn nhất của ViewHolder khi ta có thể cải thiện hiệu năng dựa vào việc không phải khởi tạo quá nhiều View cùng một lúc nữa.

Hy vọng ví dụ vừa rồi có thể làm sáng tỏ 1 ít về các khái niệm đã nêu ở trên. Giờ thì tiếp tục với việc đào sâu hơn.


RecyclerView - Hoạt động như thế nào?

Quy trình hoạt động:

Image reference: Microsoft docs

Nôm na, Adapter sẽ thực hiện việc gán data được lấy từ bộ data vào các view và chuyển việc kiểm soát(sắp xếp) các view đó cho Layout Manager.

RecyclerView sẽ không khởi tạo từng view cho từng item có trong bộ data. Thay vào đó, nó chỉ khởi tạo đúng bằng với số lượng item view có thể hiển thị trên màn hình(Viewport) và tái sử dụng lại các view đó khi người dùng scroll. Ngay khi item view đầu tiên bị che khuất(scroll), nó sẽ đi qua quá trình tái chế và được sử dụng lại như ở hình trên:

  • Ngay khi một view bị khuất khỏi màn hình sau khi scroll, nó trở thành một scrap view

Để hiểu hơn về Scrap view. Recycler có một cơ chế bộ đệm Scrap Heap cho các view này:

Nó là một lightweight collection dành cho những view đã được scroll ra khỏi ViewPort nhưng vẫn có khả năng được "visible" trở lại ngay lập tức(chẳng hạn như view ngay trước item đầu tiên hoặc view ngay sau item cuối cùng trong Viewport - gọi là các Detached View). Những view nằm bên trong danh sách Scrap Heap này có thể được trả lại cho LayoutManager sử dụng để hiển thị ngay lập tức mà không cần phải truyền lại cho Adapter xử lý(gọi lại onBindViewHolder()). Lý do là vì data được gán với view này vẫn còn nguyên và đúng vị trí. Điều này giúp làm giảm được thời gian xử lý đáng kể thông qua việc không cần phải gán data trở lại.

Thế nếu những view nằm trong Scrap Heap không còn là Detached View nữa thì sao? Chúng sẽ đi đâu?

Câu trả lời là chúng sẽ được chuyển tới Recycle Pool.

  • Khi cần hiển thị một item mới, một view sẽ được lấy ra và tái sử dụng từ Recycle Pool. Và vì lí do các view này vẫn chứa phần data cũ của item ở vị trí khác nên chúng phải được gán trở lại, chúng được gọi là các dirty view.

Tương tự Scrap Heap, chúng ta có thêm một khái niệm bộ đệm khác của RecyclerView là Recycle Pool:

Recycle Pool là một collection của các view chứa data bẩn(data từ item ở một vị trí khác trong bộ data) - hay còn gọi là dirty view. Những view này luôn luôn được truyền lại cho Adapter thông qua lời gọi hàm onBindViewHolder() để gán lại data đúng với vị trí đó và tiếp tục chuyển tới LayoutManager để thực hiện việc sắp xếp.

LayoutManager có thể request view từ Recycler để thực hiện việc sắp xếp và hiển thị. Nhiệm vụ của Recycler là lấy view từ Scrap Heap hoặc Recycle Pool hay có thể tạo mới hoàn toàn thông qua lời gọi onCreateViewHolder(), sau đó trả lại cho LayoutManager.

  • Tái sử dụng dirty view: Adapter xác định data cần hiển thị cho item tiếp theo và bind data này vào phần view. Tham chiếu đến các view này được lấy từ ViewHolder.
  • View đã được tái sử dụng sẽ được thêm vào danh sách các item view chuẩn bị được hiển thị lên màn hình.
  • View đã được tái sử dụng hiển thị lên màn hình khi user scroll đến. Trong khi đó, một view khác được scroll ra khỏi ViewPort sẽ được tái chế theo như các bước nêu trên.

Mỗi lần Adapter inflate một item-layout, nó cũng đồng thời khởi tạo một ViewHolder tương ứng. Bên trong ViewHolder sẽ sử dụng findViewById hoặc view binding để lấy các tham chiếu tới các view nằm bên trong layout. Những tham chiếu này(TextView, ImaegView,...) sẽ được sử dụng để load những data mới mỗi khi layout đó được tái sử dụng để hiển thị data mới

Tóm lại, luồng hoạt động chính của RecyclerView khi người dùng scroll sẽ là:

  1. Tìm và lấy view trong Scrap Heap để hiển thị nếu view đó nằm ngay cạnh ViewBound(ngay trước item đầu hoặc ngay sau item cuối đang hiển thị trên màn hình). Và nếu không phải, chuyển view đó cho Recycle Pool.
  2. Tìm và lấy view trong Recycle Pool, view này sẽ được gán lại dữ liệu sao cho đúng vị trí thông qua lời gọi hàm onBindViewHolder().
  3. Và nếu số lượng view trong Recycle Pool không đáp ứng được nhu cầu hiển thị(hết view có thể tái sử dụng), onCreateViewHolder() sẽ được gọi để tạo thêm view và gán tiếp tục dữ liệu cho view đó.

Thế thì cái gì sẽ đóng vai trò như một người thuyền trưởng, và sử dụng(gọi) các phương thức nằm trong luồng hoạt động đã nêu trên?


RecyclerView - Các phương thức

Khi implement một RecyclerView.Adapter, chúng ta phải override lại các phương thức sau:

Image reference: Google

  • getItemCount(): báo cáo số lượng item cho RecyclerView.
  • onCreateViewHolder(): Tạo một instance ViewHolder mới.
  • onBindViewHodler(): Gán data với vị trí tương ứng cho item view.

LayoutManager sẽ gọi các phương thức này khi xắp sếp các item bên trong RecycleView

Nếu LayoutManager thất bại trong việc tìm một view thích hợp bên trong Scrap Heap hoặc Recycle Pool, nó sẽ khởi tạo một view mới hoàn toàn thông qua lời gọi hàm onCreateViewHolder(). Sau đó nó sẽ bind data vào view đó thông qua onBindViewHolder() nếu cần thiết, và cuối cùng là sắp xếp tuỳ theo loại LayoutManager.


RecyclerView - Thực thi các thay đổi

  • NotifyDataSetChanged: Báo hiệu toàn bộ data set đã thay đổi(ép thay đổi toàn bộ danh sách view).
  • NotifyItemChanged: Báo hiệu có sự thay đổi của item view ở 1 vị trí cụ thể.
  • NotifyItemRemoved: Báo hiệu item ở 1 vị trí cụ thể đã được loại bỏ khỏi danh sách.

Tương tự với notifyItemRangeInserted, notifyItemRangeRemoved, notifyItemRangeChanged. Tìm hiểu và sử dụng tuỳ theo từng trường hợp cụ thể mà ứng dụng yêu cầu. Chúng ta có thể gọi một trong các hàm ở trên để refresh lại RecyclerView theo cách tối ưu nhất nếu biết data đã thay đổi như thế nào.


RecyclerView: Một số mẹo về hiệu năng

  • recyclerView.setHasFixedSize(true): Nếu RecyclerView mang 1 kích thước nhất định(match_parent, width/height cố định) thì chúng ta có thể set recyclerView.setHasFixedSize(true). Bằng cách này, chúng ta báo cho RecyclerView biết là kích thước của nó sẽ không bị ảnh hưởng bởi nội dung bên trong Adapter(kích thước item view thay đổi) và RecyclerView sẽ không cần phải invalidate lại toàn bộ layout khi nội dung bên trong Adapter có sự thay đổi. RecyclerView vẫn có thể thay đổi kích thước của nó dựa vào một số yếu tố khác(kích thước của parent)
  • recyclerView.setItemViewCacheSize(size): Set số lượng view nằm ngoài ViewBound mà RecyclerView có thể giữ lại trước khi chuyển các view đó cho RecycledPool. Khi người dùng được scroll và có 1 view nào đó chỉ gần như hoàn toàn nằm ngoài ViewBound. RecyclerView sẽ giữ view đó lại và không tái chế nó, và người dùng có thể scroll lại view đó ngay mà không cần phải chạy lại hàmonBindViewHolder()
  • Hãy xem qua adapter.setHasStableIds(true): Hàm này có vài tác dụng khi làm việc với animation và visual của item view.

Tham khảo

Bài viết gốc được publish ở đây

Vì đây là lần đầu tiên mình viết bài, nên nếu mình viết có phần nào hơi khó hiểu, các bạn có thể cùng mình thảo luận bên dưới nhé!

Rất cảm ơn các bạn đã đọc bài viết!

UTH(Under The Hood): dịch thô sẽ là bên dưới cái mũ. Có nghĩa là cách hoạt động bên trong của một loại công nghệ nào đó.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí