Last active
April 8, 2024 15:40
-
-
Save ElianFabian/6eac3bddf19adfa63d16329d6a9b7f85 to your computer and use it in GitHub Desktop.
RecyclerView adapter utils
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>) {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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