Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: WIP: Rough version of paging and multiplexed models #568

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ abstract class PagedListEpoxyController<T>(
* 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<EpoxyModel<*>>

override fun onModelBound(
holder: EpoxyViewHolder,
Expand All @@ -94,7 +94,7 @@ abstract class PagedListEpoxyController<T>(
previouslyBoundModel: EpoxyModel<*>?
) {
// TODO the position may not be a good value if there are too many injected items.
modelCache.loadAround(position)
modelCache.loadAround(boundModel)
}

/**
Expand All @@ -119,4 +119,4 @@ abstract class PagedListEpoxyController<T>(
override fun areContentsTheSame(oldItem: Any?, newItem: Any?) = oldItem == newItem
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
private val modelBuilder: (itemIndex: Int, item: T?) -> EpoxyModel<*>,
private val modelBuilder: (itemIndex: Int, item: T?) -> List<EpoxyModel<*>>,
private val rebuildCallback: () -> Unit,
private val itemDiffCallback : DiffUtil.ItemCallback<T>,
private val diffExecutor : Executor? = null,
Expand All @@ -43,7 +45,10 @@ internal class PagedListModelCache<T>(
/**
* Backing list for built models. This is a full array list that has null items for not yet build models.
*/
private val modelCache = arrayListOf<EpoxyModel<*>?>()
private val modelCache = linkedMapOf<EpoxyModel<*>, ItemPosition>()

private val unbuiltModels = mutableSetOf<ItemPosition>()

/**
* Tracks the last accessed position so that we can report it back to the paged list when models are built.
*/
Expand All @@ -53,31 +58,36 @@ internal class PagedListModelCache<T>(
* 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()
}
}

Expand Down Expand Up @@ -122,36 +132,47 @@ internal class PagedListModelCache<T>(
asyncDiffer.submitList(pagedList)
}

private fun getOrBuildModel(pos: Int): EpoxyModel<*> {
modelCache[pos]?.let {
return it
private fun getOrBuildModels(pos: ItemPosition): List<EpoxyModel<*>> {
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<EpoxyModel<*>> {
unbuiltModels.forEach {
getOrBuildModels(it)
}
(0 until modelCache.size).forEach {
getOrBuildModel(it)
getOrBuildModels(it)
}
lastPosition?.let {
triggerLoadAround(it)
}
@Suppress("UNCHECKED_CAST")
return modelCache as List<EpoxyModel<*>>
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))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<User>(
modelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {
override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
override fun buildItemModel(currentPosition: Int, item: User?): List<EpoxyModel<*>> {
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}")
)
}
}

Expand All @@ -67,6 +83,9 @@ class TestController : PagedListEpoxyController<User>(

init {
isDebugLoggingEnabled = true
addInterceptor {
Log.d("rdhruva", "Items in model list: ${it.size}")
}
}

override fun onExceptionSwallowed(exception: RuntimeException) {
Expand Down