diff --git a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/BaseRvAdapter.kt b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/BaseRvAdapter.kt index d83b9d2e8..bc7b38559 100644 --- a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/BaseRvAdapter.kt +++ b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/BaseRvAdapter.kt @@ -13,13 +13,12 @@ import kotlinx.collections.immutable.toImmutableList * @since 1.0.0 */ abstract class BaseRvAdapter private constructor( - private val delegate: IRvAdapterDelegate.Impl>, + private val delegate: IMutableRvAdapter.Impl>, ) : RecyclerView.Adapter(), IRvBinding, - IMutableRvAdapter, - IRvAdapterDelegate by delegate { + IMutableRvAdapter by delegate { - constructor() : this(IRvAdapterDelegate.Impl()) { + constructor() : this(IMutableRvAdapter.Impl()) { @Suppress("LeakingThis") delegate.adapter = this } diff --git a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapter.kt b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapter.kt index 3eba3cd5a..49eaf1a80 100644 --- a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapter.kt +++ b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapter.kt @@ -1,8 +1,27 @@ package io.goooler.demoapp.adapter.rv.core +import android.view.ViewGroup +import androidx.annotation.LayoutRes import androidx.databinding.BindingAdapter import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import io.goooler.demoapp.adapter.rv.core.ISpanSize.Companion.SPAN_SIZE_FULL + +/** + * Keep the same signature as [RecyclerView.Adapter]. + * + * Workaround for [KT-21955](https://youtrack.jetbrains.com/issue/KT-21955). + */ +internal interface RecyclerViewAdapter { + fun onAttachedToRecyclerView(recyclerView: RecyclerView) + fun onDetachedFromRecyclerView(recyclerView: RecyclerView) + fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH + fun onBindViewHolder(holder: BindingViewHolder, position: Int) + fun onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: List) + fun getItemViewType(position: Int): Int +} /** * Created on 2020/10/21. @@ -13,7 +32,7 @@ import androidx.recyclerview.widget.RecyclerView * @version 1.0.0 * @since 1.0.0 */ -internal interface IRvAdapter { +internal interface IRvAdapter : RecyclerViewAdapter { /** * Get data list. @@ -39,9 +58,119 @@ internal interface IRvAdapter { * Get item by position. */ operator fun get(position: Int): M? + + @Suppress("TooManyFunctions") + open class Impl : IRvAdapter + where AP : IRvAdapter, + AP : IRvBinding, + AP : RecyclerView.Adapter { + + private val ivdManager = ViewTypeDelegateManager() + + @Suppress("PropertyName", "ktlint:standard:backing-property-naming") + protected val _list = mutableListOf() + + lateinit var adapter: AP + + override val list: List get() = _list + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + initManager(ivdManager) + fixSpanSize(recyclerView) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + ivdManager.clear() + recyclerView.adapter = null + } + + override operator fun get(position: Int): M = _list.getOrElse(position) { + // Override get in adapters as a fallback. + adapter[position] ?: error("No such a element in position $position in adapter $adapter.") + } + + @LayoutRes + override fun getItemViewType(position: Int): Int = get(position).viewType + + override fun onCreateViewHolder( + parent: ViewGroup, + @LayoutRes viewType: Int, + ): BindingViewHolder = BindingViewHolder.create(parent, viewType).also { + adapter.onCreateVHForAll(it.binding) + ivdManager.onCreateVH(it.binding, viewType) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + onBindViewHolder(holder, position, emptyList()) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: List) { + get(position).let { + setFullSpan(holder, it) + adapter.onBindVHForAll(holder.binding, it, payloads) + ivdManager.onBindVH(holder.binding, it, payloads) + holder.binding.executePendingBindings() + } + } + + override fun onCreateVHForAll(binding: ViewDataBinding) { + adapter.onCreateVH(binding) + } + + override fun onBindVHForAll(binding: ViewDataBinding, model: M, payloads: List) { + adapter.onBindVH(binding, model, payloads) + } + + /** + * Transform data list. Always return a new list. + */ + fun transform(original: List): List { + val result = mutableListOf() + original.forEach { findLeaf(it, result) } + return result + } + + /** + * Recursively traversing all leaf nodes. + */ + @Suppress("UNCHECKED_CAST") + private fun findLeaf(model: M, list: MutableList) { + if (model is IVhModelWrapper<*>) { + if (model.viewType != -1) list += model + model.subList.forEach { findLeaf(it as M, list) } + } else { + list += model + } + } + + /** + * Fix span size when recyclerView's layoutManager is [GridLayoutManager]. + */ + private fun fixSpanSize(recyclerView: RecyclerView) { + (recyclerView.layoutManager as? GridLayoutManager)?.let { + it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + (get(position) as? ISpanSize)?.spanSize?.let { size -> + return if (size == SPAN_SIZE_FULL) it.spanCount else size + } + return it.spanCount + } + } + } + } + + /** + * Set full span when recyclerView's layoutManager is [StaggeredGridLayoutManager]. + */ + private fun setFullSpan(holder: RecyclerView.ViewHolder, item: M) { + (holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.let { + it.isFullSpan = (item as? ISpanSize)?.spanSize == SPAN_SIZE_FULL + } + } + } } -internal interface IMutableRvAdapter : IRvAdapter { +internal interface IMutableRvAdapter : IRvAdapter { /** * Set or Get data list. @@ -56,16 +185,65 @@ internal interface IMutableRvAdapter : IRvAdapter { fun removeItem(index: Int) fun removeItem(item: M) + + class Impl : + IRvAdapter.Impl(), + IMutableRvAdapter + where AP : IRvAdapter, + AP : IRvBinding, + AP : RecyclerView.Adapter { + + override var list: List + get() = _list + set(value) { + _list.clear() + _list.addAll(transform(value)) + } + + override fun refreshItems(items: List) { + refreshItems(items, adapter::notifyItemChanged) + } + + override fun removeItem(index: Int) { + removeItem(index = index, adapter::notifyItemRemoved) + } + + override fun removeItem(item: M) { + removeItem(item = item, adapter::notifyItemRemoved) + } + + /** + * Compare the list to find the same items and refresh them. + */ + private inline fun refreshItems(items: List, notify: (Int) -> Unit) { + transform(items).forEach { + if (it in _list) { + notify(_list.indexOf(it)) + } + } + } + + private inline fun removeItem(index: Int, notify: (Int) -> Unit) { + _list.removeAt(index) + notify(index) + } + + private inline fun removeItem(item: M, notify: (Int) -> Unit) { + _list.indexOf(item).takeIf { it != -1 }?.let { + removeItem(it, notify) + } + } + } } @BindingAdapter("binding_rv_dataList") internal fun RecyclerView.bindingSetList(list: List?) { @Suppress("UNCHECKED_CAST") - (adapter as? IMutableRvAdapter)?.list = list.orEmpty() + (adapter as? IMutableRvAdapter)?.list = list.orEmpty() } @BindingAdapter("binding_rv_refreshItems") internal fun RecyclerView.bindingRefreshItems(items: List?) { @Suppress("UNCHECKED_CAST") - (adapter as? IMutableRvAdapter)?.refreshItems(items.orEmpty()) + (adapter as? IMutableRvAdapter)?.refreshItems(items.orEmpty()) } diff --git a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapterDelegate.kt b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapterDelegate.kt deleted file mode 100644 index 7e4e7c8f3..000000000 --- a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/core/IRvAdapterDelegate.kt +++ /dev/null @@ -1,175 +0,0 @@ -package io.goooler.demoapp.adapter.rv.core - -import android.view.ViewGroup -import androidx.annotation.LayoutRes -import androidx.databinding.ViewDataBinding -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.StaggeredGridLayoutManager -import io.goooler.demoapp.adapter.rv.core.ISpanSize.Companion.SPAN_SIZE_FULL - -/** - * Keep the same signature as [RecyclerView.Adapter]. - * - * Workaround for [KT-21955](https://youtrack.jetbrains.com/issue/KT-21955). - */ -internal interface RecyclerViewAdapter { - fun onAttachedToRecyclerView(recyclerView: RecyclerView) - fun onDetachedFromRecyclerView(recyclerView: RecyclerView) - fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH - fun onBindViewHolder(holder: BindingViewHolder, position: Int) - fun onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: List) - fun getItemViewType(position: Int): Int -} - -internal interface IRvAdapterDelegate : - IMutableRvAdapter, - RecyclerViewAdapter { - - @Suppress("TooManyFunctions") - class Impl : IRvAdapterDelegate - where AP : IRvAdapter, - AP : IRvBinding, - AP : RecyclerView.Adapter { - - private val ivdManager = ViewTypeDelegateManager() - private val _list = mutableListOf() - - lateinit var adapter: AP - - override var list: List - get() = _list - set(value) { - _list.clear() - _list.addAll(transform(value)) - } - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - initManager(ivdManager) - fixSpanSize(recyclerView) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - ivdManager.clear() - recyclerView.adapter = null - } - - override operator fun get(position: Int): M = _list.getOrElse(position) { - // Override get in adapters as a fallback. - adapter[position] ?: error("No such a element in position $position in adapter $adapter.") - } - - @LayoutRes - override fun getItemViewType(position: Int): Int = get(position).viewType - - override fun onCreateViewHolder( - parent: ViewGroup, - @LayoutRes viewType: Int, - ): BindingViewHolder = BindingViewHolder.create(parent, viewType).also { - adapter.onCreateVHForAll(it.binding) - ivdManager.onCreateVH(it.binding, viewType) - } - - override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { - onBindViewHolder(holder, position, emptyList()) - } - - override fun onBindViewHolder(holder: BindingViewHolder, position: Int, payloads: List) { - get(position).let { - setFullSpan(holder, it) - adapter.onBindVHForAll(holder.binding, it, payloads) - ivdManager.onBindVH(holder.binding, it, payloads) - holder.binding.executePendingBindings() - } - } - - override fun onCreateVHForAll(binding: ViewDataBinding) { - adapter.onCreateVH(binding) - } - - override fun onBindVHForAll(binding: ViewDataBinding, model: M, payloads: List) { - adapter.onBindVH(binding, model, payloads) - } - - override fun refreshItems(items: List) { - refreshItems(items, adapter::notifyItemChanged) - } - - override fun removeItem(index: Int) { - removeItem(index = index, adapter::notifyItemRemoved) - } - - override fun removeItem(item: M) { - removeItem(item = item, adapter::notifyItemRemoved) - } - - /** - * Transform data list. Always return a new list. - */ - fun transform(original: List): List { - val result = mutableListOf() - original.forEach { findLeaf(it, result) } - return result - } - - /** - * Compare the list to find the same items and refresh them. - */ - private inline fun refreshItems(items: List, notify: (Int) -> Unit) { - transform(items).forEach { - if (it in _list) { - notify(_list.indexOf(it)) - } - } - } - - private inline fun removeItem(index: Int, notify: (Int) -> Unit) { - _list.removeAt(index) - notify(index) - } - - private inline fun removeItem(item: M, notify: (Int) -> Unit) { - _list.indexOf(item).takeIf { it != -1 }?.let { - removeItem(it, notify) - } - } - - /** - * Recursively traversing all leaf nodes. - */ - @Suppress("UNCHECKED_CAST") - private fun findLeaf(model: M, list: MutableList) { - if (model is IVhModelWrapper<*>) { - if (model.viewType != -1) list += model - model.subList.forEach { findLeaf(it as M, list) } - } else { - list += model - } - } - - /** - * Fix span size when recyclerView's layoutManager is [GridLayoutManager]. - */ - private fun fixSpanSize(recyclerView: RecyclerView) { - (recyclerView.layoutManager as? GridLayoutManager)?.let { - it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - (get(position) as? ISpanSize)?.spanSize?.let { size -> - return if (size == SPAN_SIZE_FULL) it.spanCount else size - } - return it.spanCount - } - } - } - } - - /** - * Set full span when recyclerView's layoutManager is [StaggeredGridLayoutManager]. - */ - private fun setFullSpan(holder: RecyclerView.ViewHolder, item: M) { - (holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.let { - it.isFullSpan = (item as? ISpanSize)?.spanSize == SPAN_SIZE_FULL - } - } - } -} diff --git a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/diff/BaseRvDiffAdapter.kt b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/diff/BaseRvDiffAdapter.kt index 631ed3293..faafb4ba5 100644 --- a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/diff/BaseRvDiffAdapter.kt +++ b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/diff/BaseRvDiffAdapter.kt @@ -5,7 +5,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import io.goooler.demoapp.adapter.rv.core.BindingViewHolder import io.goooler.demoapp.adapter.rv.core.IMutableRvAdapter -import io.goooler.demoapp.adapter.rv.core.IRvAdapterDelegate import io.goooler.demoapp.adapter.rv.core.IRvBinding import kotlinx.collections.immutable.toImmutableList @@ -20,21 +19,20 @@ import kotlinx.collections.immutable.toImmutableList */ abstract class BaseRvDiffAdapter private constructor( asyncDifferConfig: AsyncDifferConfig, - private val delegate: IRvAdapterDelegate.Impl>, + private val delegate: IMutableRvAdapter.Impl>, ) : ListAdapter(asyncDifferConfig), IRvBinding, - IMutableRvAdapter, - IRvAdapterDelegate by delegate { + IMutableRvAdapter by delegate { constructor(callback: DiffCallback = DiffCallback()) : this( AsyncDifferConfig.Builder(callback).build(), - IRvAdapterDelegate.Impl(), + IMutableRvAdapter.Impl(), ) { @Suppress("LeakingThis") delegate.adapter = this } - constructor(config: AsyncDifferConfig) : this(config, IRvAdapterDelegate.Impl()) { + constructor(config: AsyncDifferConfig) : this(config, IMutableRvAdapter.Impl()) { @Suppress("LeakingThis") delegate.adapter = this } diff --git a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/paging/BaseRvPagingAdapter.kt b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/paging/BaseRvPagingAdapter.kt index 2635cf6d4..8245e9c31 100644 --- a/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/paging/BaseRvPagingAdapter.kt +++ b/adapter/src/main/kotlin/io/goooler/demoapp/adapter/rv/paging/BaseRvPagingAdapter.kt @@ -7,7 +7,6 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import io.goooler.demoapp.adapter.rv.core.BindingViewHolder import io.goooler.demoapp.adapter.rv.core.IRvAdapter -import io.goooler.demoapp.adapter.rv.core.IRvAdapterDelegate import io.goooler.demoapp.adapter.rv.core.IRvBinding import io.goooler.demoapp.adapter.rv.diff.DiffCallback import io.goooler.demoapp.adapter.rv.diff.IDiffVhModelType @@ -21,11 +20,10 @@ import io.goooler.demoapp.adapter.rv.diff.IDiffVhModelType */ abstract class BaseRvPagingAdapter private constructor( callback: DiffCallback, - private val delegate: IRvAdapterDelegate.Impl>, + private val delegate: IRvAdapter.Impl>, ) : PagingDataAdapter(callback), IRvBinding, - IRvAdapter, - IRvAdapterDelegate by delegate { + IRvAdapter by delegate { var onLoadStatusListener: OnLoadStatusListener? = null @@ -33,7 +31,7 @@ abstract class BaseRvPagingAdapter private constructor( get() = snapshot().items set(_) = error("You shouldn't call this setter. Use submitData() instead.") - constructor(callback: DiffCallback = DiffCallback()) : this(callback, IRvAdapterDelegate.Impl()) { + constructor(callback: DiffCallback = DiffCallback()) : this(callback, IRvAdapter.Impl()) { @Suppress("LeakingThis") delegate.adapter = this }