diff --git a/build.gradle b/build.gradle
index 8b6d36f..fbc932c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,4 +26,5 @@ dependencies {
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "android.arch.lifecycle:extensions:$lifecycleVersion"
+ implementation "android.arch.paging:runtime:$pagingVersion"
}
diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/DelegatePagedAdapter.kt b/src/main/java/ru/touchin/roboswag/components/adapters/DelegatePagedAdapter.kt
new file mode 100644
index 0000000..ca9b2d6
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/components/adapters/DelegatePagedAdapter.kt
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package ru.touchin.roboswag.components.adapters
+
+import android.arch.paging.PagedList
+import android.arch.paging.PagedListAdapter
+import android.support.v7.recyclerview.extensions.ListAdapter
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+
+import java.util.ArrayList
+import java.util.Collections
+import java.util.LinkedList
+
+import io.reactivex.functions.BiConsumer
+import io.reactivex.functions.Consumer
+import ru.touchin.roboswag.components.utils.UiUtils
+import ru.touchin.roboswag.components.utils.lifecycle.Stopable
+import android.support.v7.util.DiffUtil
+import ru.touchin.roboswag.core.log.Lc
+import ru.touchin.roboswag.core.observables.collections.ObservableCollection
+import ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException
+
+/**
+ * Created by Gavriil Sitnikov on 20/11/2015.
+ * Adapter based on [PagedListAdapter] and providing some useful features like:
+ * - item-based binding method;
+ * - delegates by [AdapterDelegate] over itemViewType logic;
+ * - item click listener setup by [.setOnItemClickListener];
+ * - allows to inform about footers/headers by overriding base create/bind methods and [.getHeadersCount] plus [.getFootersCount];
+ *
+ * @param Type of items to bind to ViewHolders;
+ * @param Type of ViewHolders to show items.
+ */
+abstract//TooManyMethods: it's ok
+class DelegatePagedAdapter(
+ val stopable: Stopable,
+ diffCallback: DiffUtil.ItemCallback) : PagedListAdapter(diffCallback) {
+
+ companion object {
+ /**
+ * Enables debugging features like checking concurrent delegates.
+ */
+ var inDebugMode: Boolean = false
+ }
+
+ private var onItemClickListener: Any? = null
+ private var itemClickDelayMillis: Long = 0
+ private val attachedRecyclerViews = LinkedList()
+ private val delegates = ArrayList>()
+
+ /**
+ * Headers count goes before items.
+ */
+ protected val headersCount: Int = 0
+
+ /**
+ * Footers count goes after items and headers.
+ */
+ protected val footersCount: Int = 0
+
+ override fun submitList(list: PagedList?) {
+ super.submitList(list)
+// items = list ?: listOf()
+ }
+
+ override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+ super.onAttachedToRecyclerView(recyclerView)
+ attachedRecyclerViews.add(recyclerView)
+ }
+
+ private fun anyRecyclerViewShown(): Boolean = attachedRecyclerViews.any { it.isShown }
+
+ override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView)
+ attachedRecyclerViews.remove(recyclerView)
+ }
+
+ /**
+ * Returns list of added delegates.
+ *
+ * @return List of [AdapterDelegate].
+ */
+ fun getDelegates(): List> = Collections.unmodifiableList(delegates)
+
+ /**
+ * Adds [ItemAdapterDelegate] to adapter.
+ *
+ * @param delegate Delegate to add.
+ */
+ fun addDelegate(delegate: ItemAdapterDelegate) {
+ addDelegateInternal(delegate)
+ }
+
+ /**
+ * Adds [PositionAdapterDelegate] to adapter.
+ *
+ * @param delegate Delegate to add.
+ */
+ fun addDelegate(delegate: PositionAdapterDelegate) {
+ addDelegateInternal(delegate)
+ }
+
+ private fun addDelegateInternal(delegate: AdapterDelegate) {
+ if (inDebugMode) {
+ for (addedDelegate in delegates) {
+ if (addedDelegate.itemViewType == delegate.itemViewType) {
+ Lc.assertion("AdapterDelegate with viewType=" + delegate.itemViewType + " already added")
+ return
+ }
+ }
+ }
+ delegates.add(delegate)
+ notifyDataSetChanged()
+ }
+
+ /**
+ * Removes [AdapterDelegate] from adapter.
+ *
+ * @param delegate Delegate to remove.
+ */
+ fun removeDelegate(delegate: AdapterDelegate) {
+ delegates.remove(delegate)
+ notifyDataSetChanged()
+ }
+
+ private fun checkDelegates(alreadyPickedDelegate: AdapterDelegate<*>?, currentDelegate: AdapterDelegate<*>) {
+ if (alreadyPickedDelegate != null) {
+ throw ShouldNotHappenException("Concurrent delegates: $currentDelegate and $alreadyPickedDelegate")
+ }
+ }
+
+ private fun getItemPositionInCollection(positionInAdapter: Int): Int {
+ val shiftedPosition = positionInAdapter - headersCount
+ return if (shiftedPosition >= 0 && shiftedPosition < super.getItemCount()) shiftedPosition else -1
+ }
+
+ override//Complexity: because of debug code
+ fun getItemViewType(positionInAdapter: Int): Int {
+ var delegateOfViewType: AdapterDelegate<*>? = null
+ val positionInCollection = getItemPositionInCollection(positionInAdapter)
+ val item = if (positionInCollection >= 0) getItem(positionInCollection) else null
+ for (delegate in delegates) {
+ if (delegate is ItemAdapterDelegate<*, *>) {
+ if (item != null && delegate.isForViewType(item, positionInAdapter, positionInCollection)) {
+ checkDelegates(delegateOfViewType, delegate)
+ delegateOfViewType = delegate
+ if (!inDebugMode) {
+ break
+ }
+ }
+ } else if (delegate is PositionAdapterDelegate<*>) {
+ if (delegate.isForViewType(positionInAdapter)) {
+ checkDelegates(delegateOfViewType, delegate)
+ delegateOfViewType = delegate
+ if (!inDebugMode) {
+ break
+ }
+ }
+ } else {
+ Lc.assertion("Delegate of type " + delegate.javaClass)
+ }
+ }
+
+ return if (delegateOfViewType != null) delegateOfViewType.itemViewType else super.getItemViewType(positionInAdapter)
+ }
+
+ override fun getItemId(positionInAdapter: Int): Long {
+ val result = LongContainer()
+ tryDelegateAction(positionInAdapter,
+ BiConsumer { itemAdapterDelegate, positionInCollection ->
+ result.value = itemAdapterDelegate.getItemId(getItem(positionInCollection)!!,
+ positionInAdapter, positionInCollection)
+ },
+ Consumer { positionAdapterDelegate -> result.value = positionAdapterDelegate.getItemId(positionInAdapter) },
+ Consumer { positionInCollection -> result.value = super.getItemId(positionInAdapter) })
+ return result.value
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun tryDelegateAction(positionInAdapter: Int,
+ itemAdapterDelegateAction: BiConsumer, Int>,
+ positionAdapterDelegateAction: Consumer>,
+ defaultAction: Consumer) {
+ val viewType = getItemViewType(positionInAdapter)
+ val positionInCollection = getItemPositionInCollection(positionInAdapter)
+ for (delegate in delegates) {
+ if (delegate is ItemAdapterDelegate<*, *>) {
+ if (positionInCollection >= 0 && viewType == delegate.getItemViewType()) {
+ try {
+ itemAdapterDelegateAction.accept(delegate as ItemAdapterDelegate, positionInCollection)
+ } catch (exception: Exception) {
+ Lc.assertion(exception)
+ }
+
+ return
+ }
+ } else if (delegate is PositionAdapterDelegate<*>) {
+ if (viewType == delegate.getItemViewType()) {
+ try {
+ positionAdapterDelegateAction.accept(delegate as PositionAdapterDelegate)
+ } catch (exception: Exception) {
+ Lc.assertion(exception)
+ }
+
+ return
+ }
+ } else {
+ Lc.assertion("Delegate of type " + delegate.javaClass)
+ }
+ }
+ try {
+ defaultAction.accept(positionInCollection)
+ } catch (exception: Exception) {
+ Lc.assertion(exception)
+ }
+
+ }
+
+ override fun getItemCount(): Int = headersCount + super.getItemCount() + footersCount
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TItemViewHolder {
+ for (delegate in delegates) {
+ if (delegate.itemViewType == viewType) {
+ return delegate.onCreateViewHolder(parent) as TItemViewHolder
+ }
+ }
+ throw ShouldNotHappenException("Add some AdapterDelegate or override this method")
+ }
+
+ override fun onBindViewHolder(holder: TItemViewHolder, positionInAdapter: Int) {
+ tryDelegateAction(positionInAdapter,
+ BiConsumer { itemAdapterDelegate, positionInCollection ->
+ bindItemViewHolder(itemAdapterDelegate, holder, getItem(positionInCollection)!!, null, positionInAdapter, positionInCollection)
+ },
+ Consumer { positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter) },
+ Consumer { positionInCollection ->
+ if (positionInCollection >= 0) {
+ bindItemViewHolder(null, holder, getItem(positionInCollection)!!, null, positionInAdapter, positionInCollection)
+ }
+ })
+ }
+
+ override fun onBindViewHolder(holder: TItemViewHolder, positionInAdapter: Int, payloads: List) {
+ super.onBindViewHolder(holder, positionInAdapter, payloads)
+ tryDelegateAction(positionInAdapter,
+ BiConsumer { itemAdapterDelegate, positionInCollection ->
+ bindItemViewHolder(itemAdapterDelegate, holder, getItem(positionInCollection)!!,
+ payloads, positionInAdapter, positionInCollection)
+ },
+ Consumer { positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter) },
+ Consumer { positionInCollection ->
+ if (positionInCollection >= 0) {
+ bindItemViewHolder(null, holder, getItem(positionInCollection)!!,
+ payloads, positionInAdapter, positionInCollection)
+ }
+ })
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun bindItemViewHolder(itemAdapterDelegate: ItemAdapterDelegate?,
+ holder: BindableViewHolder, item: TItem, payloads: List?,
+ positionInAdapter: Int, positionInCollection: Int) {
+ val itemViewHolder: TItemViewHolder
+ try {
+ itemViewHolder = holder as TItemViewHolder
+ } catch (exception: ClassCastException) {
+ Lc.assertion(exception)
+ return
+ }
+
+ updateClickListener(holder, item, positionInAdapter, positionInCollection)
+ if (itemAdapterDelegate != null) {
+ if (payloads == null) {
+ itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, positionInAdapter, positionInCollection)
+ } else {
+ itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, payloads, positionInAdapter, positionInCollection)
+ }
+ } else {
+ if (payloads == null) {
+ onBindItemToViewHolder(itemViewHolder, positionInAdapter, item)
+ } else {
+ onBindItemToViewHolder(itemViewHolder, positionInAdapter, item, payloads)
+ }
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun updateClickListener(holder: BindableViewHolder, item: TItem,
+ positionInAdapter: Int, positionInCollection: Int) {
+ if (onItemClickListener != null && !isOnClickListenerDisabled(item, positionInAdapter, positionInCollection)) {
+ UiUtils.setOnRippleClickListener(holder.itemView,
+ { any ->
+ when (onItemClickListener) {
+ is OnItemClickListener<*> -> (onItemClickListener as OnItemClickListener).onItemClicked(item)
+ is OnItemWithPositionClickListener<*> -> (onItemClickListener as OnItemWithPositionClickListener)
+ .onItemClicked(item, positionInAdapter, positionInCollection)
+ else -> Lc.assertion("Unexpected onItemClickListener type " + onItemClickListener!!)
+ }
+ },
+ itemClickDelayMillis)
+ }
+ }
+
+ /**
+ * Method to bind item (from [.items]) to item-specific ViewHolder.
+ * It is not calling for headers and footer which counts are returned by [.getHeadersCount] and @link #getFootersCount()}.
+ * You don't need to override this method if you have delegates for every view type.
+ *
+ * @param holder ViewHolder to bind item to;
+ * @param positionInAdapter Position of ViewHolder (NOT item!);
+ * @param item Item returned by position (WITH HEADER OFFSET!).
+ */
+ protected fun onBindItemToViewHolder(holder: TItemViewHolder, positionInAdapter: Int, item: TItem) {
+ // do nothing by default - let delegates do it
+ }
+
+ /**
+ * Method to bind item (from [.items]) to item-specific ViewHolder with payloads.
+ * It is not calling for headers and footer which counts are returned by [.getHeadersCount] and @link #getFootersCount()}.
+ *
+ * @param holder ViewHolder to bind item to;
+ * @param positionInAdapter Position of ViewHolder in adapter (NOT item!);
+ * @param item Item returned by position (WITH HEADER OFFSET!);
+ * @param payloads Payloads.
+ */
+ protected fun onBindItemToViewHolder(holder: TItemViewHolder, positionInAdapter: Int, item: TItem,
+ payloads: List) {
+ // do nothing by default - let delegates do it
+ }
+
+ public override fun getItem(positionInAdapter: Int): TItem? {
+ val positionInCollection = getItemPositionInCollection(positionInAdapter)
+ return if (positionInCollection >= 0) super.getItem(positionInCollection) else null
+ }
+
+ /**
+ * Sets item click listener.
+ *
+ * @param onItemClickListener Item click listener.
+ */
+ fun setOnItemClickListener(onItemClickListener: OnItemClickListener?) {
+ this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY)
+ }
+
+ /**
+ * Sets item click listener.
+ *
+ * @param onItemClickListener Item click listener;
+ * @param itemClickDelayMillis Delay of calling click listener.
+ */
+ fun setOnItemClickListener(onItemClickListener: OnItemClickListener?, itemClickDelayMillis: Long) {
+ this.onItemClickListener = onItemClickListener
+ this.itemClickDelayMillis = itemClickDelayMillis
+ }
+
+ /**
+ * Sets item click listener.
+ *
+ * @param onItemClickListener Item click listener.
+ */
+ fun setOnItemClickListener(onItemClickListener: OnItemWithPositionClickListener?) {
+ this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY)
+ }
+
+ /**
+ * Sets item click listener.
+ *
+ * @param onItemClickListener Item click listener;
+ * @param itemClickDelayMillis Delay of calling click listener.
+ */
+ fun setOnItemClickListener(onItemClickListener: OnItemWithPositionClickListener?, itemClickDelayMillis: Long) {
+ this.onItemClickListener = onItemClickListener
+ this.itemClickDelayMillis = itemClickDelayMillis
+ }
+
+ /**
+ * Returns if click listening disabled or not for specific item.
+ *
+ * @param item Item to check click availability;
+ * @param positionInAdapter Position of clicked item in adapter (with headers);
+ * @param positionInCollection Position of clicked item in inner collection;
+ * @return True if click listener enabled for such item.
+ */
+ fun isOnClickListenerDisabled(item: TItem, positionInAdapter: Int, positionInCollection: Int): Boolean = false
+
+ /**
+ * Interface to simply add item click listener.
+ *
+ * @param Type of item
+ */
+ interface OnItemClickListener {
+
+ /**
+ * Calls when item have clicked.
+ *
+ * @param item Clicked item.
+ */
+ fun onItemClicked(item: TItem)
+
+ }
+
+ /**
+ * Interface to simply add item click listener based on item position in adapter and collection.
+ *
+ * @param Type of item
+ */
+ interface OnItemWithPositionClickListener {
+
+ /**
+ * Calls when item have clicked.
+ *
+ * @param item Clicked item;
+ * @param positionInAdapter Position of clicked item in adapter (with headers);
+ * @param positionInCollection Position of clicked item in inner collection.
+ */
+ fun onItemClicked(item: TItem, positionInAdapter: Int, positionInCollection: Int)
+
+ }
+
+ private data class LongContainer(var value: Long = 0)
+
+}