Ngày nay, Jetpack Compose đã được chấp nhận và nó chắc chắn xứng đáng với điều đó. Đó là một sự thay đổi mô hình thực sự trong phát triển giao diện người dùng Android . Quá nhiều việc tạo danh sách soạn sẵn đã bị loại bỏ ở đó. Và nó thật đẹp.
Tôi tin rằng nhiều dự án vẫn đang sử dụng khung Xem và do đó sử dụng RecyclerView để có danh sách.
Nhưng dù sao đi nữa, Compose là tương lai và đây là lý do tại sao tôi sử dụng từ “cổ điển” trong tiêu đề.
Vintage có nghĩa là cũ nhưng vàng. Bài viết này là kết quả tổng hợp kinh nghiệm của tôi với RecyclerView và đối với những người vẫn đang theo đuổi nó, tôi sẽ kể về tầm nhìn của mình trong việc xây dựng một khung xung quanh RecyclerView.
Khung nên cung cấp một mức độ lành mạnh của khả năng mở rộng. Vì vậy, các kịch bản chính nên được xử lý với số lượng mẫu soạn sẵn tối thiểu, nhưng nó sẽ không cản trở nếu người dùng muốn tạo thứ gì đó tùy chỉnh.
Được biết, phương pháp hay nhất trong quá trình làm việc với RecyclerView là sử dụng API DiffUtil
để cập nhật dữ liệu trong danh sách. Chúng tôi sẽ không phát minh lại một bánh xe ở đây, vì vậy chúng tôi sẽ tập trung vào việc tận dụng DiffUtil
và cũng sẽ cố gắng giảm bớt một buổi lễ thực hiện nó.
Việc có nhiều loại chế độ xem trong một danh sách là điều tự nhiên. Màn hình của thiết bị di động bị giới hạn bởi chiều cao và các thiết bị khác nhau có kích thước màn hình khác nhau. Vì vậy, ngay cả khi nội dung của bạn phù hợp với màn hình trên một thiết bị mà không cần cuộn, thì nội dung đó có thể cần được cuộn trên một thiết bị khác.
Đó là lý do tại sao bạn nên phát triển một số màn hình trên đầu RecyclerView.
Trang trí là một trường hợp rất phổ biến cho danh sách. Và mặc dù bạn có thể hợp nhất các đồ trang trí với bố cục vật phẩm, những gì có thể trông đơn giản, nhưng nó không linh hoạt lắm: cùng loại vật phẩm có thể yêu cầu các đồ trang trí khác nhau ở các màn hình khác nhau hoặc thậm chí các đồ trang trí khác nhau tùy thuộc vào vị trí vật phẩm trong danh sách.
Tùy chọn đầu tiên là làm mọi thứ từ đầu. Nó trao toàn quyền kiểm soát việc triển khai, nhưng chúng tôi luôn có thể làm được.
Có một loạt các khung nguồn mở của bên thứ ba cho phép hoạt động với RecyclerView với ít bản soạn sẵn hơn. Tôi sẽ không liệt kê tất cả, nhưng muốn chỉ ra hai trong số chúng với cách tiếp cận hoàn toàn trái ngược nhau:
Giải pháp từ AirBnb để tạo các màn hình phức tạp với RecyclerView. Nó đang thêm một loạt các API mới và thậm chí là tạo mã để giảm bớt bản soạn sẵn nhiều nhất có thể.
Nhưng tôi không thể thoát khỏi cảm giác rằng mã kết quả trông có vẻ xa lạ. Ngoài ra, một xử lý chú thích thêm một lớp phức tạp khác.
Thư viện nhỏ, thực sự cung cấp một loạt chức năng và giao diện ngắn gọn chỉ gợi ý cho bạn cách phân chia danh sách không đồng nhất của bạn thành tập hợp các bộ điều hợp được ủy quyền. Trên thực tế, bạn có thể tạo giải pháp riêng như vậy trong thời gian tương đối ngắn.
Tôi thích các giải pháp bản địa nhỏ, đủ đơn giản để giữ tất cả các hành vi trong tầm tay và không che giấu tất cả các chi tiết triển khai. Đó là lý do tại sao tôi tin rằng AdapterDelagates và các nguyên tắc của nó là một ứng cử viên sáng giá cho nền tảng của khuôn khổ của chúng tôi.
Điều rất cơ bản để bắt đầu là một khai báo chung của các mục danh sách. Danh sách các mục trong thế giới thực rất khác nhau, nhưng điều duy nhất chúng tôi tự tin – chúng tôi có thể so sánh nó.
Tôi khuyên bạn nên tham khảo API của DiffUtil.ItemCallback
interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } }
Đó là một khai báo tối giản (và do đó chắc chắn, IMO) của một mục danh sách trong khung. Ngữ nghĩa của các phương thức được dự định giống với DiffUtil bằng cách sử dụng:
isItemTheSame
kiểm tra danh tính của other
và this
isContentTheSame
kiểm tra dữ liệu trong other
và this
Cách phổ biến nhất để cung cấp danh tính ổn định và duy nhất là sử dụng số nhận dạng đến từ máy chủ.
Vì vậy, hãy có một triển khai trừu tượng để giảm bớt một chút bản tóm tắt có thể có:
abstract class DefaultListItem : ListItem { abstract val id: String override fun isItemTheSame(other: ListItem): Boolean = when { other !is DefaultListItem -> false this::class != other::class -> false else -> this.id == other.id } }
isContentTheSame
trong phần lớn các trường hợp sẽ khá đơn giản (kiểm tra tính bằng nhau), nếu việc triển khai ListItem sẽ là một data class
.
Giữ ListItem
riêng biệt vẫn hợp lý trong trường hợp bạn có các mục trong danh sách không chiếu dữ liệu máy chủ và không có bất kỳ danh tính lành mạnh nào. Ví dụ: nếu bạn có chân trang và đầu trang dưới dạng các mục trong danh sách hoặc bạn có một mục thuộc loại nào đó:
data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem }
Cách tiếp cận được đề xuất cho phép chúng tôi triển khai gọi lại DiffUtil
rất tự nhiên và đơn lẻ:
object DefaultDiffUtil : DiffUtil.ItemCallback<ListItem>() { override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isItemTheSame(newItem) override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isContentTheSame(newItem) }
Và bước cuối cùng ở đây là khai báo bộ điều hợp mặc định mở rộng AsyncListDifferDelegationAdapter
từ AdapterDelegates:
class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil)
Thư viện AdapterDelegates cung cấp một cách thuận tiện để khai báo các đại biểu cho một kiểu xem cụ thể. Điều duy nhất chúng tôi có thể cải thiện là giảm một chút bản soạn sẵn:
inline fun <reified I : ListItem, V : ViewBinding> defaultAdapterDelegate( noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, noinline block: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit ) = adapterDelegateViewBinding<I, ListItem, V>(viewBinding = viewBinding, block = block)
Đối với mục đích giới thiệu, chúng ta hãy xem xét một ví dụ về cách khai báo một số TitleListItem
, chứa một trường văn bản có thể trông như thế nào:
data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem()
và ủy quyền cho nó
fun titleItemDelegate(onClick: ((String) -> Unit)) = defaultAdapterDelegate< TitleListItem, TitleListItemBinding >(viewBinding = { inflater, root -> TitleListItemBinding.inflate(inflater,root,false) }) { itemView.setOnClickListener { it(item.id) } bind { binding.root.text = item.title } } }
Ban đầu, do API của RecyclerView
, cài đặt trang trí được thực hiện riêng biệt với cài đặt dữ liệu vào danh sách. Nếu bạn có một số logic để trang trí như: all items should have offset at the bottom except the last one
hoặc all items should have a divider at the bottom, but headers should have an offset at bottom
thì việc tạo trang trí trở nên khó khăn.
Thường thì các đội đến với trang trí "thần thánh" có thể được định cấu hình:
class SmartDividerItemDecorator( val context: Context, val skipDividerFor: Set<Int> = emptySet(), val showDividerAfterLastItem: Boolean = false, val showDividerBeforeFirstItem: Boolean = false, val dividerClipToPadding: Boolean = true, val dividerPaddingLeft: Int = 0, val dividerPaddingRight: Int = 0 ) : ItemDecoration()
Bạn chỉ có thể tưởng tượng các chi tiết triển khai và mức độ mong manh và không thể mở rộng của nó.
Chúng ta có thể sử dụng một cách tiếp cận cũ đã được chứng minh bằng cách bắt chước một số API RecyclerView
và ủy quyền triển khai.
interface Decoration
Đánh dấu giao diện để trang trí, sẽ được trình bày trong tương lai.
interface HasDecorations { var decorations: List<Decoration> }
Giao diện này nên được triển khai bởi các mục dữ liệu của chúng tôi để tuyên bố rằng mục cụ thể này muốn được trang trí bằng một danh sách các trang trí nhất định. Các đồ trang trí có var
vì mục đích đơn giản là thay đổi nó trong thời gian chạy, nếu cần.
Thông thường, một vật phẩm có một trang trí duy nhất, vì vậy để giảm bớt bản mẫu bằng cách gói một vật phẩm duy nhất vào listOf()
chúng ta có thể thực hiện thao tác như sau:
interface HasDecoration : HasDecorations { var decoration: Decoration override var decorations: List<Decoration> get() = listOf(decoration) set(value) { decoration = when { value.isEmpty() -> None value.size == 1 -> value.first() else -> throw IllegalArgumentException("Applying few decorations to HasDecoration instance is prohibited. Use HasDecorations") } } }
Giai đoạn tiếp theo bắt chước API ItemDecoration
:
interface DecorationDrawer<T : Decoration> { fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) fun onDraw( c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) }
Như bạn có thể thấy, nó hoàn toàn lặp lại ItemDecoration
với một điểm khác biệt duy nhất – bây giờ nó đã biết về kiểu trang trí và phiên bản sắp ra mắt.
Các triển khai có thể có cho DecorationDrawer
là chủ đề của bài viết thứ hai, bây giờ chúng ta hãy chỉ tập trung vào giao diện và cách xử lý nó để trình bày đồ trang trí.
class CompositeItemDecoration( private val context: Context ) : RecyclerView.ItemDecoration() { private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // [1] private fun applyDecorationToView(parent: RecyclerView, view: View, applyDecoration: (Decoration) -> Unit) { val position = getAdapterPositionForView(parent, view) // [2] when { // [3] position == RecyclerView.NO_POSITION -> return adapter.items[position] is HasDecorations -> { val decoration = (adapter.items[position] as HasDecorations).decorations decoration.forEach(applyDecoration) } else -> return } } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { applyDecorationToView(parent, view) { decoration -> val drawer = getDrawerFor(decoration) drawer.getItemOffsets(outRect, view, parent, state, decoration) } } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.forEach { child -> applyDecorationToView(parent, child) { decoration -> val drawer = getDrawerFor(decoration) drawer.onDraw(c, child, parent, state, decoration) } } } private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> { // [4] } private fun getAdapterPositionForView(parent: RecyclerView, view: View): Int { var position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) { val oldPosition = parent.getChildViewHolder(view).oldPosition if (oldPosition in 0 until (parent.adapter?.itemCount ?: 0)) { position = oldPosition } } return position } }
getAdapterPositionForView()
không phải là thứ cụ thể để áp dụng trang trí, nó chỉ là một phương pháp để giải quyết chính xác vị trí bộ điều hợp cho chế độ xem nhất địnhHasDecorations
Bài viết này đã phát hiện ra một cách tiếp cận để xây dựng khung danh sách xung quanh RecyclerView
. Ủy quyền được sử dụng làm nguyên tắc cơ bản để gói các mục trong danh sách và trang trí danh sách.
bản địa. Việc triển khai không phụ thuộc vào thư viện của bên thứ ba, điều này có thể làm giảm tính linh hoạt trong tương lai. AdapterDelegates được chọn để xem xét chính xác các tiêu chí này – nó rất đơn giản và tự nhiên và tất cả các hành vi đều nằm trong tầm tay của bạn.
Khả năng mở rộng. Nhờ nguyên tắc ủy quyền và phân tách mối quan tâm, chúng tôi có thể tách rời việc triển khai các mục đại biểu hoặc ngăn kéo trang trí.
Thật dễ dàng để tích hợp dần dần vào một dự án hiện có
Trong phần thứ hai, chúng ta sẽ thảo luận về tất cả các kiểu trang trí danh sách mà chúng ta có thể thấy trong một ứng dụng Android.