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:
ganfra 2023-08-01 10:53:41 +02:00 committed by GitHub
parent b14c741422
commit 62a367520e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 373 additions and 142 deletions

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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")
}
}