diff --git a/delegates/gradle.properties b/delegates/gradle.properties index c7b782b..6bb94ac 100644 --- a/delegates/gradle.properties +++ b/delegates/gradle.properties @@ -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 \ No newline at end of file diff --git a/delegates/src/main/java/com/revolut/recyclerkit/delegates/DiffAdapter.kt b/delegates/src/main/java/com/revolut/recyclerkit/delegates/DiffAdapter.kt index 7efb299..fcb94ec 100644 --- a/delegates/src/main/java/com/revolut/recyclerkit/delegates/DiffAdapter.kt +++ b/delegates/src/main/java/com/revolut/recyclerkit/delegates/DiffAdapter.kt @@ -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 @@ -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 = AsyncListDiffer( - AdapterListUpdateCallback(this), - AsyncDifferConfig.Builder(ListDiffCallback()).build() - ) + protected interface DifferDelegate { + val items: List + fun attachRecyclerView(recyclerView: RecyclerView) + fun detachRecyclerView(recyclerView: RecyclerView) + fun setItems(items: List) + } + + private val differDelegate = if (async) { + AsyncDifferStrategy(adapter = this, autoScrollToTop = autoScrollToTop) + } else { + SyncDifferStrategy(adapter = this, autoScrollToTop = autoScrollToTop, detectMoves = true) + } + + open val items: List + get() = differDelegate.items + + override fun getItem(position: Int): ListItem = items[position] + + @UiThread + open fun setItems(items: List) { + 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 = AsyncListDiffer( + AdapterListUpdateCallback(adapter), + AsyncDifferConfig.Builder(ListDiffItemCallback()).build() + ) + private var recyclerView = WeakReference(null) + override val items: List + 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) { + 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(null) + private set + override val items = mutableListOf() + + override fun attachRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = WeakReference(recyclerView) + } + + override fun detachRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = WeakReference(null) + } + + override fun setItems(items: List) { + val diffResult = calculateDiff(items) + dispatchDiffInternal(diffResult, items, recyclerView.get() ?: error("Recycler View not attached")) + } + + protected fun dispatchDiffInternal(diffResult: DiffUtil.DiffResult, newList: List, 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) = 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): DiffUtil.DiffResult { + return DiffUtil.calculateDiff(ListDiffCallback(ArrayList(items), newList), detectMoves) + } + + } - private class ListDiffCallback : DiffUtil.ItemCallback() { + private class ListDiffItemCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean { return oldItem.listId == newItem.listId @@ -56,4 +172,45 @@ class DiffAdapter( } } + protected class ListDiffCallback( + private val oldList: List, + private val newList: List + ) : 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 } diff --git a/rxdiffadapter/gradle.properties b/rxdiffadapter/gradle.properties index 84fbae9..ee3b3c6 100644 --- a/rxdiffadapter/gradle.properties +++ b/rxdiffadapter/gradle.properties @@ -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 \ No newline at end of file diff --git a/rxdiffadapter/src/main/java/com/revolut/rxdiffadapter/RxDiffAdapter.kt b/rxdiffadapter/src/main/java/com/revolut/rxdiffadapter/RxDiffAdapter.kt index 04f2208..787dd42 100644 --- a/rxdiffadapter/src/main/java/com/revolut/rxdiffadapter/RxDiffAdapter.kt +++ b/rxdiffadapter/src/main/java/com/revolut/rxdiffadapter/RxDiffAdapter.kt @@ -3,18 +3,15 @@ package com.revolut.rxdiffadapter import android.animation.ValueAnimator import android.os.Looper import androidx.annotation.UiThread -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.revolut.recyclerkit.delegates.AbsRecyclerDelegatesAdapter import com.revolut.recyclerkit.delegates.DelegatesManager +import com.revolut.recyclerkit.delegates.DiffAdapter import com.revolut.recyclerkit.delegates.ListItem import com.revolut.recyclerkit.delegates.RecyclerViewDelegate import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers -import java.lang.ref.WeakReference import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit @@ -47,7 +44,7 @@ open class RxDiffAdapter @Deprecated("Replace with constructor without delegates val async: Boolean = false, private val autoScrollToTop: Boolean = false, private val detectMoves: Boolean = true -) : AbsRecyclerDelegatesAdapter(delegatesManager) { +) : DiffAdapter(delegatesManager = delegatesManager, async = async, autoScrollToTop = autoScrollToTop) { companion object { @@ -68,38 +65,24 @@ open class RxDiffAdapter @Deprecated("Replace with constructor without delegates delegatesManager = DelegatesManager(delegates).also { Preconditions.checkForDuplicateDelegates(delegates) } ) - private class Queue( - val processor: PublishProcessor, - val disposable: Disposable - ) - interface ListWrapper : List { fun clear() operator fun set(index: Int, element: T): T fun addAll(elements: Collection): Boolean } - private class CopyOnWriteListWrapper : CopyOnWriteArrayList(), ListWrapper - private class ArrayListListWrapper : ArrayList(), ListWrapper - - val items: ListWrapper = if (async) CopyOnWriteListWrapper() else ArrayListListWrapper() - - private var recyclerView = WeakReference(null) - private var queue: Queue>? = null + private interface RxDifferDelegate : DifferDelegate { + override val items: ListWrapper + fun onDetachFromWindow() + } - private fun createQueue(): Queue> = PublishProcessor.create>().let { - Queue( - processor = it, - disposable = it.onBackpressureLatest().throttleLast(ValueAnimator.getFrameDelay(), TimeUnit.MILLISECONDS) - .observeOn(Schedulers.single()) - .map { newList -> calculateDiff(newList) } - .observeOn(AndroidSchedulers.mainThread(), false, 1) - .subscribe { (diffResult, newList) -> dispatchDiffInternal(diffResult, newList) } - ) + private val differDelegate: RxDifferDelegate = if (async) { + RxAsyncDifferStrategy(this, autoScrollToTop, detectMoves) + } else { + RxSyncDifferStrategy(this, autoScrollToTop, detectMoves) } - private fun getOrCreateQueue(): Queue> = - queue?.takeUnless { it.disposable.isDisposed } ?: createQueue().apply { queue = this } + override val items: ListWrapper = differDelegate.items open fun updateItem(index: Int, item: ListItem) { if (index < itemCount) { @@ -114,89 +97,79 @@ open class RxDiffAdapter @Deprecated("Replace with constructor without delegates } @UiThread - open fun setItems(items: List) { + override fun setItems(items: List) { check(Looper.myLooper() == Looper.getMainLooper()) { "RxDiffAdapter.setItems() was called from worker thread" } - - if (async) { - getOrCreateQueue().processor.onNext(items) - } else { - calculateDiff(items).let { (diffResult, newList) -> dispatchDiffInternal(diffResult, newList) } - } + differDelegate.setItems(items) } - fun onDetachedFromWindow() = queue?.disposable?.dispose() - - private fun dispatchDiffInternal(diffResult: DiffUtil.DiffResult, newList: List) { - val rv = recyclerView.get() ?: error("Recycler View not attached") - - val firstVisiblePosition = when (val lm = rv.layoutManager) { - is LinearLayoutManager -> lm.findFirstCompletelyVisibleItemPosition() - else -> 0 - } - - val dispatchDiff: () -> Unit = { - this.items.clear() - this.items.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - if (rv.isComputingLayout) { - rv.post { dispatchDiff() } - } else { - dispatchDiff() - } - - if (autoScrollToTop && firstVisiblePosition == 0) { - rv.scrollToPosition(0) - } + fun onDetachedFromWindow() { + differDelegate.onDetachFromWindow() } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - super.onAttachedToRecyclerView(recyclerView) check(!(async && (recyclerView !is AsyncDiffRecyclerView))) { "RxDiffAdapter in async mode must be used with AsyncDiffRecyclerView" } - this.recyclerView = WeakReference(recyclerView) + differDelegate.attachRecyclerView(recyclerView) } - private fun calculateDiff(newList: List): Pair> { - val diffResult = DiffUtil.calculateDiff(ListDiffCallback(this.items.toList(), newList), detectMoves) - return diffResult to newList + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + differDelegate.detachRecyclerView(recyclerView) } override fun getItem(position: Int): ListItem = items[position] override fun getItemCount(): Int = items.size - private class ListDiffCallback( - private val oldList: List, - private val newList: List - ) : DiffUtil.Callback() { - - override fun getOldListSize(): Int = oldList.size + private class RxAsyncDifferStrategy( + adapter: RecyclerView.Adapter<*>, + autoScrollToTop: Boolean, + detectMoves: Boolean + ) : SyncDifferStrategy(adapter, autoScrollToTop, detectMoves), RxDifferDelegate { + private class CopyOnWriteListWrapper : CopyOnWriteArrayList(), ListWrapper - override fun getNewListSize(): Int = newList.size + private class Queue( + val processor: PublishProcessor, + val disposable: Disposable + ) - 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 - } + private var queue: Queue>? = null + + private fun createQueue(): Queue> = PublishProcessor.create>().let { + Queue( + processor = it, + disposable = it.onBackpressureLatest().throttleLast(ValueAnimator.getFrameDelay(), TimeUnit.MILLISECONDS) + .observeOn(Schedulers.single()) + .map { newList -> calculateDiff(newList) to newList } + .observeOn(AndroidSchedulers.mainThread(), false, 1) + .subscribe { (diffResult, newList) -> + val rv = recyclerView.get() ?: error("Recycler View not attached") + dispatchDiffInternal(diffResult, newList, rv) + } + ) } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldList[oldItemPosition]?.equals(newList[newItemPosition]) ?: false + private fun getOrCreateQueue(): Queue> = + queue?.takeUnless { it.disposable.isDisposed } ?: createQueue().apply { queue = this } - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val oldData = oldList[oldItemPosition] as Any - val newData = newList[newItemPosition] as Any + override val items = CopyOnWriteListWrapper() - return if (newData is ListItem) { - newData.calculatePayload(oldData) - } else { - null - } + override fun setItems(items: List) { + getOrCreateQueue().processor.onNext(items) + } + + override fun onDetachFromWindow() { + queue?.disposable?.dispose() } } + + private class RxSyncDifferStrategy( + adapter: RecyclerView.Adapter<*>, + autoScrollToTop: Boolean, + detectMoves: Boolean + ) : SyncDifferStrategy(adapter, autoScrollToTop, detectMoves), RxDifferDelegate { + private class ArrayListListWrapper : ArrayList(), ListWrapper + + override val items = ArrayListListWrapper() + + override fun onDetachFromWindow() = Unit + } }