Skip to content

Commit

Permalink
Added autoScrollToTop and async to DiffAdapter (#23)
Browse files Browse the repository at this point in the history
* Added abstract methods in AbsRecyclerDelegatesAdapter moving them from RxDiffAdapter

* Added DefaultDiffAdapter that calculates diff on main thread

* Removed old deprecated RxDiffAdapter constructor

* Simplified constructor

* Removed DefaultDiffAdapter. Added autoScrollToTop into DiffAdapter

* Reverted AbsRecyclerDelegatesAdapter changes

* Reverted rxdiffadapter version bump

* Added async parameter to DiffAdapter. RxDiffAdapter extends DiffAdapter

* DiffAdapter with async and autoScrollToTop

* Added DifferDelegate inside RxDiffAdapter

* Fixed an issue

* Unified scroll to 0 position when isComputingLayout and not.
Using unified extension method to calculate first visible position in recycler view
  • Loading branch information
RKuanysh committed Jan 23, 2024
1 parent 076190e commit e94f968
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 102 deletions.
2 changes: 1 addition & 1 deletion delegates/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
POM_ARTIFACT_ID=delegates
VERSION_NAME=1.1.1
VERSION_NAME=1.1.2
POM_NAME=delegates
POM_PACKAGING=jar
GROUP=com.revolut.recyclerkit
177 changes: 167 additions & 10 deletions delegates/src/main/java/com/revolut/recyclerkit/delegates/DiffAdapter.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.revolut.recyclerkit.delegates

import android.annotation.SuppressLint
import android.os.Looper
import androidx.annotation.UiThread
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import java.lang.ref.WeakReference

/*
* Copyright (C) 2019 Revolut
Expand All @@ -25,22 +31,132 @@ import androidx.recyclerview.widget.DiffUtil
*
*/

class DiffAdapter(
delegatesManager: DelegatesManager = DelegatesManager()
open class DiffAdapter(
delegatesManager: DelegatesManager = DelegatesManager(),
async: Boolean = true,
autoScrollToTop: Boolean = false
) : AbsRecyclerDelegatesAdapter(delegatesManager) {

private val differ: AsyncListDiffer<ListItem> = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(ListDiffCallback<ListItem>()).build()
)
protected interface DifferDelegate {
val items: List<ListItem>
fun attachRecyclerView(recyclerView: RecyclerView)
fun detachRecyclerView(recyclerView: RecyclerView)
fun setItems(items: List<ListItem>)
}

private val differDelegate = if (async) {
AsyncDifferStrategy(adapter = this, autoScrollToTop = autoScrollToTop)
} else {
SyncDifferStrategy(adapter = this, autoScrollToTop = autoScrollToTop, detectMoves = true)
}

open val items: List<ListItem>
get() = differDelegate.items

override fun getItem(position: Int): ListItem = items[position]

@UiThread
open fun setItems(items: List<ListItem>) {
check(Looper.myLooper() == Looper.getMainLooper()) { "DiffAdapter.setItems() was called from worker thread" }
differDelegate.setItems(items)
}

override fun getItemCount() = items.size

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
differDelegate.attachRecyclerView(recyclerView)
}

override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
differDelegate.detachRecyclerView(recyclerView)
}

private class AsyncDifferStrategy(
adapter: RecyclerView.Adapter<*>,
private val autoScrollToTop: Boolean
) : DifferDelegate {
private val differ: AsyncListDiffer<ListItem> = AsyncListDiffer(
AdapterListUpdateCallback(adapter),
AsyncDifferConfig.Builder(ListDiffItemCallback<ListItem>()).build()
)
private var recyclerView = WeakReference<RecyclerView>(null)
override val items: List<ListItem>
get() = differ.currentList

override fun attachRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = WeakReference(recyclerView)
}

override fun detachRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = WeakReference(null)
}

override fun setItems(items: List<ListItem>) {
val recyclerViewRef = recyclerView
val rv = recyclerViewRef.get() ?: error("Recycler View not attached")

val firstVisiblePosition = rv.layoutManager.findFirstCompletelyVisibleItemPosition(autoScrollToTop)

if (firstVisiblePosition == 0) {
differ.submitList(items) {
recyclerViewRef.get()?.scrollToPosition(0)
}
} else {
differ.submitList(items)
}
}
}

protected open class SyncDifferStrategy(
private val adapter: RecyclerView.Adapter<*>,
private val autoScrollToTop: Boolean,
private val detectMoves: Boolean,
) : DifferDelegate {
protected var recyclerView = WeakReference<RecyclerView>(null)
private set
override val items = mutableListOf<ListItem>()

override fun attachRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = WeakReference(recyclerView)
}

override fun detachRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = WeakReference(null)
}

override fun setItems(items: List<ListItem>) {
val diffResult = calculateDiff(items)
dispatchDiffInternal(diffResult, items, recyclerView.get() ?: error("Recycler View not attached"))
}

protected fun dispatchDiffInternal(diffResult: DiffUtil.DiffResult, newList: List<ListItem>, recyclerView: RecyclerView) {
val firstVisiblePosition = recyclerView.layoutManager.findFirstCompletelyVisibleItemPosition(autoScrollToTop)

override fun getItem(position: Int): ListItem = differ.currentList[position]
val dispatchDiff: () -> Unit = {
items.clear()
items.addAll(newList)

fun setItems(items: List<ListItem>) = differ.submitList(items)
diffResult.dispatchUpdatesTo(adapter)

override fun getItemCount() = differ.currentList.size
if (firstVisiblePosition == 0) {
recyclerView.scrollToPosition(0)
}
}

if (recyclerView.isComputingLayout) {
recyclerView.post { dispatchDiff() }
} else {
dispatchDiff()
}
}

protected fun calculateDiff(newList: List<ListItem>): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(ListDiffCallback(ArrayList(items), newList), detectMoves)
}

}

private class ListDiffCallback<T> : DiffUtil.ItemCallback<ListItem>() {
private class ListDiffItemCallback<T> : DiffUtil.ItemCallback<ListItem>() {

override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
return oldItem.listId == newItem.listId
Expand All @@ -56,4 +172,45 @@ class DiffAdapter(
}
}

protected class ListDiffCallback<T>(
private val oldList: List<T>,
private val newList: List<T>
) : DiffUtil.Callback() {

override fun getOldListSize(): Int = oldList.size

override fun getNewListSize(): Int = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return if (oldItem is ListItem && newItem is ListItem) {
oldItem.listId == newItem.listId
} else {
oldItem == newItem
}
}

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldList[oldItemPosition]?.equals(newList[newItemPosition]) ?: false

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldData = oldList[oldItemPosition] as Any
val newData = newList[newItemPosition] as Any

return if (newData is ListItem) {
newData.calculatePayload(oldData)
} else {
null
}
}
}
}

private fun LayoutManager?.findFirstCompletelyVisibleItemPosition(autoScrollToTop: Boolean): Int = if (autoScrollToTop) {
when (this) {
is LinearLayoutManager -> findFirstCompletelyVisibleItemPosition()
else -> 0
}
} else {
-1
}
2 changes: 1 addition & 1 deletion rxdiffadapter/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
POM_ARTIFACT_ID=rxdiffadapter
VERSION_NAME=1.1.0
VERSION_NAME=1.1.1
POM_NAME=rxdiffadapter
POM_PACKAGING=jar
GROUP=com.revolut.recyclerkit
Loading

0 comments on commit e94f968

Please sign in to comment.