RoomList: use same logic than Timeline for caching built items. (#1013)
* RoomList: use same logic than Timeline for caching built items. Extract into reusable components. * RoomList: fix tests * Fix `DiffCacheUpdater` docs --------- Co-authored-by: ganfra <francoisg@element.io> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
b14c741422
commit
62a367520e
11 changed files with 373 additions and 142 deletions
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* 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 io.element.android.libraries.androidutils.diff
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
/**
|
||||
* Default implementation of [DiffUtil.Callback] that uses [areItemsTheSame] to compare items.
|
||||
*/
|
||||
internal class DefaultDiffCallback<T>(
|
||||
private val oldList: List<T>,
|
||||
private val newList: List<T>,
|
||||
private val areItemsTheSame: (oldItem: T?, newItem: T?) -> Boolean,
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.getOrNull(oldItemPosition)
|
||||
val newItem = newList.getOrNull(newItemPosition)
|
||||
return areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.getOrNull(oldItemPosition)
|
||||
val newItem = newList.getOrNull(newItemPosition)
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* 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 io.element.android.libraries.androidutils.diff
|
||||
|
||||
/**
|
||||
* A cache that can be used to store some data that can be invalidated when a diff is applied.
|
||||
* The cache is invalidated by the [DiffCacheInvalidator].
|
||||
*/
|
||||
interface DiffCache<E> {
|
||||
fun get(index: Int): E?
|
||||
fun indices(): IntRange
|
||||
fun isEmpty(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A [DiffCache] that can be mutated by adding, removing or updating elements.
|
||||
*/
|
||||
interface MutableDiffCache<E> : DiffCache<E> {
|
||||
fun removeAt(index: Int): E?
|
||||
fun add(index: Int, element: E?)
|
||||
operator fun set(index: Int, element: E?)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MutableDiffCache] backed by a [MutableList].
|
||||
*
|
||||
*/
|
||||
class MutableListDiffCache<E>(private val mutableList: MutableList<E?> = ArrayList()) : MutableDiffCache<E> {
|
||||
|
||||
override fun removeAt(index: Int): E? {
|
||||
return mutableList.removeAt(index)
|
||||
}
|
||||
|
||||
override fun get(index: Int): E? {
|
||||
return mutableList.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun indices(): IntRange {
|
||||
return mutableList.indices
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return mutableList.isEmpty()
|
||||
}
|
||||
|
||||
override operator fun set(index: Int, element: E?) {
|
||||
mutableList[index] = element
|
||||
}
|
||||
|
||||
override fun add(index: Int, element: E?) {
|
||||
mutableList.add(index, element)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* 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 io.element.android.libraries.androidutils.diff
|
||||
|
||||
/**
|
||||
* [DiffCacheInvalidator] is used to invalidate the cache when the list is updated.
|
||||
* It is used by [DiffCacheUpdater].
|
||||
* Check the default implementation [DefaultDiffCacheInvalidator].
|
||||
*/
|
||||
interface DiffCacheInvalidator<T> {
|
||||
fun onChanged(position: Int, count: Int, cache: MutableDiffCache<T>)
|
||||
|
||||
fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache<T>)
|
||||
|
||||
fun onInserted(position: Int, count: Int, cache: MutableDiffCache<T>)
|
||||
|
||||
fun onRemoved(position: Int, count: Int, cache: MutableDiffCache<T>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of [DiffCacheInvalidator].
|
||||
* It invalidates the cache by setting values to null.
|
||||
*/
|
||||
class DefaultDiffCacheInvalidator<T> : DiffCacheInvalidator<T> {
|
||||
|
||||
override fun onChanged(position: Int, count: Int, cache: MutableDiffCache<T>) {
|
||||
(position until position + count).forEach {
|
||||
// Invalidate cache
|
||||
cache[it] = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache<T>) {
|
||||
val model = cache.removeAt(fromPosition)
|
||||
cache.add(toPosition, model)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int, cache: MutableDiffCache<T>) {
|
||||
repeat(count) {
|
||||
cache.add(position, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int, cache: MutableDiffCache<T>) {
|
||||
repeat(count) {
|
||||
cache.removeAt(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* 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 io.element.android.libraries.androidutils.diff
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import timber.log.Timber
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* Class in charge of updating a [MutableDiffCache] according to the cache invalidation rules provided by the [DiffCacheInvalidator].
|
||||
* @param ListItem the type of the items in the list
|
||||
* @param CachedItem the type of the items in the cache
|
||||
* @param diffCache the cache to update
|
||||
* @param detectMoves true if DiffUtil should try to detect moved items, false otherwise
|
||||
* @param cacheInvalidator the invalidator to use to update the cache
|
||||
* @param areItemsTheSame the function to use to compare items
|
||||
*/
|
||||
class DiffCacheUpdater<ListItem, CachedItem>(
|
||||
private val diffCache: MutableDiffCache<CachedItem>,
|
||||
private val detectMoves: Boolean = false,
|
||||
private val cacheInvalidator: DiffCacheInvalidator<CachedItem> = DefaultDiffCacheInvalidator(),
|
||||
private val areItemsTheSame: (oldItem: ListItem?, newItem: ListItem?) -> Boolean,
|
||||
) {
|
||||
|
||||
private val lock = Object()
|
||||
private var prevOriginalList: List<ListItem> = emptyList()
|
||||
|
||||
private val listUpdateCallback = object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
cacheInvalidator.onInserted(position, count, diffCache)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
cacheInvalidator.onRemoved(position, count, diffCache)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
cacheInvalidator.onMoved(fromPosition, toPosition, diffCache)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
cacheInvalidator.onChanged(position, count, diffCache)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWith(newOriginalList: List<ListItem>) = synchronized(lock) {
|
||||
val timeToDiff = measureTimeMillis {
|
||||
val diffCallback = DefaultDiffCallback(prevOriginalList, newOriginalList, areItemsTheSame)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback, detectMoves)
|
||||
prevOriginalList = newOriginalList
|
||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||
}
|
||||
Timber.v("Time to apply diff on new list of ${newOriginalList.size} items: $timeToDiff ms")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue