Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active April 8, 2024 15:40
Show Gist options
  • Save ElianFabian/6eac3bddf19adfa63d16329d6a9b7f85 to your computer and use it in GitHub Desktop.
Save ElianFabian/6eac3bddf19adfa63d16329d6a9b7f85 to your computer and use it in GitHub Desktop.
RecyclerView adapter utils
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
abstract class ExtendedListAdapter<T, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH> {
val currentList: List<T> get() = _differ.currentList
private val _listener = ListListener { previousList, currentList ->
onCurrentListChanged(previousList, currentList)
}
private val _differ: AsyncListDiffer<T>
@Suppress("unused", "LeakingThis")
protected constructor(diffCallback: DiffUtil.ItemCallback<T>) {
_differ = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
)
_differ.addListListener(_listener)
}
@Suppress("unused", "LeakingThis")
protected constructor(config: AsyncDifferConfig<T>) {
_differ = AsyncListDiffer(AdapterListUpdateCallback(this), config)
_differ.addListListener(_listener)
}
@JvmOverloads
fun submitList(list: List<T>?, commitCallback: Runnable? = null) {
_differ.submitList(list, commitCallback)
}
protected fun getItem(position: Int): T {
return _differ.currentList[position]
}
protected fun getItemOrNull(position: Int): T? {
return _differ.currentList.getOrNull(position)
}
override fun getItemCount(): Int {
return _differ.currentList.size
}
fun addListListener(listener: ListListener<T>) {
_differ.addListListener(listener)
}
fun removeListListener(listener: ListListener<T>) {
_differ.removeListListener(listener)
}
protected open fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {}
}
import androidx.recyclerview.widget.AsyncListDiffer
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.suspendCancellableCoroutine
suspend fun <T> ExtendedListAdapter<T, *>.awaitCurrentListChanged(
predicate: (previousList: List<T>, currentList: List<T>) -> Boolean = { _, _ -> true },
) {
suspendCancellableCoroutine { continuation ->
val listener = object : AsyncListDiffer.ListListener<T> {
override fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {
if (predicate(previousList, currentList)) {
removeListListener(this)
continuation.resumeWith(Result.success(Unit))
}
}
}
addListListener(listener)
continuation.invokeOnCancellation {
removeListListener(listener)
}
}
}
suspend fun <T> ExtendedListAdapter<T, *>.awaitNonEmptyList() {
if (currentList.isNotEmpty()) {
return
}
awaitCurrentListChanged { _, currentList ->
currentList.isNotEmpty()
}
}
fun <T> ExtendedListAdapter<T, *>.currentListFlow(): Flow<List<T>> {
return callbackFlow {
val listener = AsyncListDiffer.ListListener { _, currentList ->
trySend(currentList)
}
addListListener(listener)
awaitClose {
removeListListener(listener)
}
}
}
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.viewbinding.ViewBinding
abstract class SelectorListAdapter<ItemT, VB : ViewBinding>(
inflate: (LayoutInflater, ViewGroup, Boolean) -> VB,
diffCallback: DiffUtil.ItemCallback<ItemT>,
var isSelectionRequired: Boolean = true,
var isSingleSelection: Boolean = true,
private val forceInitialSelectionIfRequiredAndEmpty: Boolean = true,
private val onItemSelectedChanged: (item: ItemT, isSelected: Boolean) -> Unit = { _, _ -> },
) : SimpleListAdapter<ItemT, VB>(
inflate = inflate,
diffCallback = diffCallback,
) {
var areItemsSelectable = true
private val _selectedItems = mutableListOf<ItemT>()
protected val selectedItems: List<ItemT> = _selectedItems
fun selectItemAt(position: Int) {
val item = getItem(position)
selectItem(item)
}
fun unselectItemAt(position: Int) {
val item = getItem(position)
unselectItem(item)
}
fun selectItem(item: ItemT) {
selectItemInternal(item, isUserInput = false)
}
private fun selectItemInternal(item: ItemT, isUserInput: Boolean) {
if (item in _selectedItems) {
return
}
if (isSingleSelection) {
val previousSelectedItem = _selectedItems.firstOrNull()
if (previousSelectedItem != null) {
unselectItemInternal(previousSelectedItem, isUserInput = false)
}
}
_selectedItems.add(item)
val indexOfSelectedItem = currentList.indexOf(item)
if (isUserInput) {
onItemSelectedChanged(item, true)
}
notifyItemChanged(indexOfSelectedItem)
}
fun unselectItem(item: ItemT) {
unselectItemInternal(item, isUserInput = false)
}
private fun unselectItemInternal(item: ItemT, isUserInput: Boolean) {
if (item !in _selectedItems) {
return
}
val indexOfUnselectedItem = currentList.indexOf(item)
if (isUserInput) {
onItemSelectedChanged(item, false)
}
_selectedItems.remove(item)
notifyItemChanged(indexOfUnselectedItem)
}
fun clearSelection() {
clearSelectionInternal(isUserInput = false)
}
private fun clearSelectionInternal(isUserInput: Boolean) {
if (isSelectionRequired) {
return
}
_selectedItems.forEach { item ->
if (isUserInput) {
onItemSelectedChanged(item, false)
}
notifyItemChanged(currentList.indexOf(item))
}
_selectedItems.clear()
}
fun selectAll() {
if (isSingleSelection) {
return
}
val itemsToSelect = currentList.filter { item ->
item !in _selectedItems
}
itemsToSelect.forEach { item -> selectItem(item) }
}
protected fun isItemSelected(item: ItemT): Boolean {
return item in _selectedItems
}
protected fun isItemSelectedAt(position: Int): Boolean {
return isItemSelected(getItem(position))
}
/**
* When the view is clicked or somehow interacted with to select it,
* this functions must be called in order to apply the selection logic.
*/
protected fun clickItem(item: ItemT) {
if (!areItemsSelectable) {
return
}
val isSelected = isItemSelected(item)
val areThereAnyOtherSelectedItem = _selectedItems.any { selectedItem ->
selectedItem != item
}
if (isSelected) {
if (!isSelectionRequired || (!isSingleSelection && areThereAnyOtherSelectedItem)) {
unselectItemInternal(item, isUserInput = true)
}
}
else {
selectItemInternal(item, isUserInput = true)
}
}
override fun onCurrentListChanged(previousList: List<ItemT>, currentList: List<ItemT>) {
if (
previousList.isEmpty()
&& currentList.isNotEmpty()
&& _selectedItems.isEmpty()
&& isSelectionRequired
&& forceInitialSelectionIfRequiredAndEmpty
) {
val firstItem = currentList.firstOrNull() ?: return
selectItemInternal(firstItem, isUserInput = true)
}
if (currentList.isEmpty()) {
clearSelectionInternal(isUserInput = true)
}
}
}
inline fun <T> SelectorListAdapter<T, *>.selectFirstIf(condition: (T) -> Boolean) {
val item = currentList.firstOrNull(condition) ?: return
selectItem(item)
}
inline fun <T> SelectorListAdapter<T, *>.unselectFirstIf(condition: (T) -> Boolean) {
val item = currentList.firstOrNull(condition) ?: return
unselectItem(item)
}
inline fun <T> SelectorListAdapter<T, *>.selectIf(condition: (T) -> Boolean) {
currentList.forEach { item ->
if (condition(item)) {
selectItem(item)
}
}
}
inline fun <T> SelectorListAdapter<T, *>.unselectIf(condition: (T) -> Boolean) {
currentList.forEach { item ->
if (condition(item)) {
unselectItem(item)
}
}
}
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class SingleItemListAdapter<VB : ViewBinding, ItemT : Any>(
protected val inflate: (LayoutInflater, ViewGroup, Boolean) -> VB,
diffCallback: DiffUtil.ItemCallback<ItemT>,
) : ExtendedListAdapter<ItemT, SingleItemListAdapter<VB, ItemT>.SingleItemViewHolder>(diffCallback) {
protected abstract fun onBind(holder: SingleItemViewHolder, position: Int)
protected open fun onCreateViewHolder(binding: VB): SingleItemViewHolder {
return SingleItemViewHolder(binding)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = inflate(inflater, parent, false)
return onCreateViewHolder(binding)
}
override fun onBindViewHolder(holder: SingleItemViewHolder, position: Int) {
onBind(holder, position)
}
inner class SingleItemViewHolder(val binding: VB) : RecyclerView.ViewHolder(binding.root)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment