diff --git a/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListEpoxyController.kt b/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListEpoxyController.kt index c5250ae2b8..3c0556c8b0 100644 --- a/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListEpoxyController.kt +++ b/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListEpoxyController.kt @@ -85,7 +85,7 @@ abstract class PagedListEpoxyController( * If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured * without placeholders, you don't need to handle the `null` case. */ - abstract fun buildItemModel(currentPosition: Int, item: T?): EpoxyModel<*> + abstract fun buildItemModel(currentPosition: Int, item: T?): List> override fun onModelBound( holder: EpoxyViewHolder, @@ -94,7 +94,7 @@ abstract class PagedListEpoxyController( previouslyBoundModel: EpoxyModel<*>? ) { // TODO the position may not be a good value if there are too many injected items. - modelCache.loadAround(position) + modelCache.loadAround(boundModel) } /** @@ -119,4 +119,4 @@ abstract class PagedListEpoxyController( override fun areContentsTheSame(oldItem: Any?, newItem: Any?) = oldItem == newItem } } -} \ No newline at end of file +} diff --git a/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListModelCache.kt b/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListModelCache.kt index 5308b3532c..caa9c36529 100644 --- a/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListModelCache.kt +++ b/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListModelCache.kt @@ -28,13 +28,15 @@ import com.airbnb.epoxy.EpoxyModel import java.lang.IllegalStateException import java.util.concurrent.Executor +typealias ItemPosition = Int + /** * A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches * models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is * updated. */ internal class PagedListModelCache( - private val modelBuilder: (itemIndex: Int, item: T?) -> EpoxyModel<*>, + private val modelBuilder: (itemIndex: Int, item: T?) -> List>, private val rebuildCallback: () -> Unit, private val itemDiffCallback : DiffUtil.ItemCallback, private val diffExecutor : Executor? = null, @@ -43,7 +45,10 @@ internal class PagedListModelCache( /** * Backing list for built models. This is a full array list that has null items for not yet build models. */ - private val modelCache = arrayListOf?>() + private val modelCache = linkedMapOf, ItemPosition>() + + private val unbuiltModels = mutableSetOf() + /** * Tracks the last accessed position so that we can report it back to the paged list when models are built. */ @@ -53,31 +58,36 @@ internal class PagedListModelCache( * Observer for the PagedList changes that invalidates the model cache when data is updated. */ private val updateCallback = object : ListUpdateCallback { - override fun onChanged(position: Int, count: Int, payload: Any?) { - (position until (position + count)).forEach { - modelCache[it] = null + override fun onChanged(position: ItemPosition, count: Int, payload: Any?) { + (position until (position + count)).forEach { index -> + modelCache.filterValues { it == index }.keys.forEach { model -> + modelCache.remove(model) + } } rebuildCallback() } - override fun onMoved(fromPosition: Int, toPosition: Int) { - val model = modelCache.removeAt(fromPosition) - modelCache.add(toPosition, model) - rebuildCallback() + override fun onMoved(fromPosition: ItemPosition, toPosition: ItemPosition) { + modelCache.filterValues { it == fromPosition }.keys.forEach { + modelCache[it] = toPosition + } + rebuildCallback() } - override fun onInserted(position: Int, count: Int) { - (0 until count).forEach { _ -> - modelCache.add(position, null) - } - rebuildCallback() + override fun onInserted(position: ItemPosition, count: Int) { + (position until position + count - 1).forEach { pos -> + unbuiltModels.add(pos) + } + rebuildCallback() } - override fun onRemoved(position: Int, count: Int) { - (0 until count).forEach { _ -> - modelCache.removeAt(position) + override fun onRemoved(position: ItemPosition, count: Int) { + (position until position + count - 1).forEach { index -> + modelCache.filterValues { it == index }.keys.forEach { model -> + modelCache.remove(model) } - rebuildCallback() + } + rebuildCallback() } } @@ -122,36 +132,47 @@ internal class PagedListModelCache( asyncDiffer.submitList(pagedList) } - private fun getOrBuildModel(pos: Int): EpoxyModel<*> { - modelCache[pos]?.let { - return it + private fun getOrBuildModels(pos: ItemPosition): List> { + if (pos >= asyncDiffer.currentList?.size ?: 0) { + return emptyList() } - return modelBuilder(pos, asyncDiffer.currentList?.get(pos)).also { - modelCache[pos] = it + return if (modelCache.containsValue(pos)) { + modelCache.filterValues { it == pos }.keys.toList() + } else { + modelBuilder(pos, asyncDiffer.currentList?.get(pos)).also { modelList -> + modelList.forEach { model -> + modelCache[model] = pos + } + } } } fun getModels(): List> { + unbuiltModels.forEach { + getOrBuildModels(it) + } (0 until modelCache.size).forEach { - getOrBuildModel(it) + getOrBuildModels(it) } lastPosition?.let { triggerLoadAround(it) } - @Suppress("UNCHECKED_CAST") - return modelCache as List> + Log.d("rdhruva", "Model build requested, size: ${modelCache.size}") + return modelCache.keys.toList() } - fun loadAround(position: Int) { - triggerLoadAround(position) - lastPosition = position + fun loadAround(model: EpoxyModel<*>) { + modelCache[model]?.let { itemPosition -> + triggerLoadAround(itemPosition) + lastPosition = itemPosition + } } - private fun triggerLoadAround(position: Int) { + private fun triggerLoadAround(position: ItemPosition) { asyncDiffer.currentList?.let { if (it.size > 0) { it.loadAround(Math.min(position, it.size - 1)) } } } -} \ No newline at end of file +} diff --git a/epoxy-pagingsample/src/main/java/com/airbnb/epoxy/pagingsample/PagingSampleActivity.kt b/epoxy-pagingsample/src/main/java/com/airbnb/epoxy/pagingsample/PagingSampleActivity.kt index d41ef3156c..88bede3679 100644 --- a/epoxy-pagingsample/src/main/java/com/airbnb/epoxy/pagingsample/PagingSampleActivity.kt +++ b/epoxy-pagingsample/src/main/java/com/airbnb/epoxy/pagingsample/PagingSampleActivity.kt @@ -14,6 +14,7 @@ import android.support.v7.app.AppCompatActivity import android.support.v7.widget.AppCompatTextView import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView +import android.util.Log import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.ModelView @@ -39,21 +40,36 @@ class PagingSampleActivity : AppCompatActivity() { viewModel.pagedList.observe(this, Observer { pagingController.submitList(it) }) + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + Log.d("rdhruva", "Items in RV: ${recyclerView.adapter.itemCount}") + } + } + }) } } class TestController : PagedListEpoxyController( modelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() ) { - override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: User?): List> { return if (item == null) { - PagingViewModel_() - .id(-currentPosition) - .name("loading ${currentPosition}") + listOf( + PagingViewModel_() + .id(-currentPosition) + .name("loading ${currentPosition}") + ) } else { - PagingViewModel_() - .id(item.uid) - .name("${item.uid}: ${item.firstName} / ${item.lastName}") + listOf( + PagingViewModel_() + .id(item.uid) + .name("${item.uid}: ${item.firstName} / ${item.lastName}"), + PagingViewModel_() + .id(Int.MAX_VALUE - item.uid) + .name("Second model for ${item.uid}") + ) } } @@ -67,6 +83,9 @@ class TestController : PagedListEpoxyController( init { isDebugLoggingEnabled = true + addInterceptor { + Log.d("rdhruva", "Items in model list: ${it.size}") + } } override fun onExceptionSwallowed(exception: RuntimeException) {