LazyRecycler is a library that provides LazyColumn like APIs to build lists with RecyclerView.
implementation 'io.github.dokar3:lazyrecycler:latest_version'
Then create a list by using the lazyRecycler()
function. Adapter, LayoutManager, DiffUtil, OnItemClickListener and more, these are all in one block:
lazyRecycler(recyclerView, spanCount = 3) {
// Header
item(layout = R.layout.header) {}
// Create a section
items(
items = listOfNews,
layout = ItemNewsBinding::inflate,
clicks = { view, item ->
// Handle item clicks
},
longClicks = { view, item ->
// Handle long clicks
true
},
diffCallback = diffCallback {
areItemsTheSame { oldItem, newItem ->
oldItem.id == newItem.id
}
areContentsTheSame { oldItem, newItem ->
oldItem.title == newItem.title && ...
}
},
span = { position ->
if (position % 3 == 0) 3 else 1
},
) { binding, news ->
// Bind item
}
// Some other sections
items(...)
items(...)
// Footer
item(layout = R.layout.footer) {}
}
In LazyRecycler, every item
and items
call will create a section, sections will be added to the Adapter by the creating order.
item(...) { ... }
items(...) { ... }
When creating a dynamic section, it's necessary to set a unique id to update the section later, or using reactive data sources may be a better choice (see the Reactive data sources section).
val recycler = lazyRecycler {
items(
items = news,
layout = R.layout.item_news,
id = SOME_ID,
) { ... }
}
// Update
recycler.updateSection(SOME_ID, items)
// Remove
recycler.removeSection(SOME_ID)
// xml layout id
items(items = news, layout = R.layout.item_news) { ... }
// ViewBinding, DataBinding
items(items = news, layout = ItemNewsBinding::inflate) { binding, item -> ... }
// Instantiate views
items(items = news) { parent ->
val itemView = NewsItemView(context)
...
itemView
}
For ViewBinding item/items:
items(items = news, layout = ItemNewsBinding::inflate) { binding, item ->
binding.title.text = item.title
binding.image.load(item.cover)
...
}
For DataBinding item/items:
items(items = news, layout = ItemNewsDataBinding::inflate) { binding, item ->
binding.news = item
}
For layoutId item/items:
items(items = news, layout = R.layout.item_news) { view ->
...
val tv: TextView = view.findViewById(R.id.title)
bind { item ->
tv.text = item.title
}
}
For view instantiation item/items:
items(items = news) { parent ->
val itemView = CustomNewsItemView(context)
bind { item ->
itemView.title(item.title)
...
}
itemView
}
items<I>(items = news, layout = layoutId) { ... }
items<V, I>(items = news, layout = ItemViewBinding::inflate) { ... }
items<I>(items = news) { ... }
I
for item typeV
for the View Binding
In most cases, the compiler is smart enough so type parameter(s) can be omitted.
LazyRecycler creates a LinearLayoutManager by default, if spanCount
> 1, GridLayoutManager will be used, to skip the LayoutManager setup, set setupLayoutManager
to false
:
lazyRecycler(
recyclerView,
setupLayoutManager = false,
) { ... }
....
recyclerView.layoutManager = YourOwnLayoutManager(...)
span
is used to define SpanSizeLookup
for GridLayoutManager:
items(
...,
span = { position ->
if (position == 0) 3 else 1
},
) {
...
}
To support reactive data sources like Flow
, LiveData
, or RxJava
, add the dependencies:
// Flow
implementation 'io.github.dokar3:lazyrecycler-flow:latest_version'
// LiveData
implementation 'io.github.dokar3:lazyrecycler-livedata:latest_version'
// RxJava3
implementation 'io.github.dokar3:lazyrecycler-rxjava3:latest_version'
// Paging 3
implementation 'io.github.dokar3:lazyrecycler-paging3:latest_version'
val entry: Flow<I> = ...
item(data = entry.toMutableValue(coroutineScope), ...) { ... }
val source: Flow<List<I>> = ...
items(data = source.toMutableValue(coroutineScope), ...) { ... }
val entry: LiveData<I> = ...
item(data = entry.toMutableValue(lifecycleOwner), ...) { ... }
val source: LiveData<List<I>> = ...
items(data = source.toMutableValue(lifecycleOwner), ...) { ... }
val entry: Observable<I> = ...
item(data = entry.toMutableValue(), ...) { ... }
val source: Observable<List<I>> = ...
items(data = source.toMutableValue(), ...) { ... }
To use Paging 3 with LazyRecycler, some additional setups are required.
First, create a PagingValue
from your PagingData
flow.
// Required by the AsyncPagingDataDiffer
val diffCallback = object : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean =
oldItem == newItem
}
// A PagingValue is a MutableValue so we can use it in our items() setup
val pagingValue = PagingValue(
scope = lifecycleScope,
// The PagingData flow: Flow<PagingData<Post>>
flow = viewModel.postFlow,
diffCallback = diffCallback,
)
Second, setup PagingLazyAdapter
to trigger loads when the list scrolls to the end.
lazyRecycler(
recyclerView = recyclerView,
adapterCreator = ::PagingLazyAdapter,
) { ... }
Finally, setup items
to use the paging value.
items(
data = pagingValue,
layout = ItemPostBinding::inflate,
diffCallback = pagingValue.diffCallback,
) {
// binding
}
LazyRecycler will observe the data sources automatically after attaching to the RecyclerView, so it's no necessary to call it manually. But there is a function call if really needed:
recycler.observeChanges()
When using a RxJava data source or a Flow data source which was not created by the lifecycleScope
, should stop observing data sources manually to prevent leaks:
recycler.stopObserving()
Use template()
to reuse bindings:
lazyRecycler {
// layout id template
val sectionHeader = template<String>(R.layout.section_header) {
// Bind item
bind { ... }
}
// ViewBinding template
val normalItem = template<Item>(ItemFriendBubbleBinding::inflate) { binding, item ->
// Bind item
}
item(item = "section 1", template = sectionHeader)
items(items = someItems, template = normalItem)
item(item = "section 2", template = sectionHeader)
items(items = otherItems, template = normalItem)
...
}
Use template()
+ extraViewTypes
:
lazyRecycler {
val friendBubble = template<Message>(ItemFriendBubbleBinding::inflate) { binding, msg ->
// Bind alternative items
}
items(
items = messages,
layout = ItemOwnerBubbleBinding::inflate,
extraViewTypes = listOf(
ViewType(friendBubble) { position, item -> /* predicate */ },
),
{ binding, msg ->
// Bind default items
}
...
}
Use recycler.newSections()
:
val recycler = lazyRecycler { ... }
recycler.newSections {
item(...) { ... }
items(...) { ... }
...
}
// If new sections contain any observable data source
recycler.observeChanges()
backgroundThread {
// Do not pass RecyclerView to the lazyRecycler()
val recycler = lazyRecycler {
...
}
uiThread {
recycler.attachTo(recyclerView)
}
}
Day | Night |
Day | Night |
Day | Night |