Browse Source
* 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>pull/1008/head
ganfra
1 year ago
committed by
GitHub
11 changed files with 371 additions and 140 deletions
@ -1,56 +0,0 @@
@@ -1,56 +0,0 @@
|
||||
/* |
||||
* 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.features.messages.impl.timeline.diff |
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback |
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem |
||||
import io.element.android.features.messages.impl.timeline.util.invalidateLast |
||||
import timber.log.Timber |
||||
|
||||
internal class CacheInvalidator(private val itemStatesCache: MutableList<TimelineItem?>) : |
||||
ListUpdateCallback { |
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) { |
||||
Timber.d("onChanged(position= $position, count= $count)") |
||||
(position until position + count).forEach { |
||||
// Invalidate cache |
||||
itemStatesCache[it] = null |
||||
} |
||||
} |
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) { |
||||
Timber.d("onMoved(fromPosition= $fromPosition, toPosition= $toPosition)") |
||||
val model = itemStatesCache.removeAt(fromPosition) |
||||
itemStatesCache.add(toPosition, model) |
||||
} |
||||
|
||||
override fun onInserted(position: Int, count: Int) { |
||||
Timber.d("onInserted(position= $position, count= $count)") |
||||
itemStatesCache.invalidateLast() |
||||
repeat(count) { |
||||
itemStatesCache.add(position, null) |
||||
} |
||||
} |
||||
|
||||
override fun onRemoved(position: Int, count: Int) { |
||||
Timber.d("onRemoved(position= $position, count= $count)") |
||||
itemStatesCache.invalidateLast() |
||||
repeat(count) { |
||||
itemStatesCache.removeAt(position) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,63 @@
@@ -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.features.messages.impl.timeline.diff |
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem |
||||
import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator |
||||
import io.element.android.libraries.androidutils.diff.DiffCacheInvalidator |
||||
import io.element.android.libraries.androidutils.diff.MutableDiffCache |
||||
|
||||
/** |
||||
* [DiffCacheInvalidator] implementation for [TimelineItem]. |
||||
* It uses [DefaultDiffCacheInvalidator] and invalidate the cache around the updated item so that those items are computed again. |
||||
* This is needed because a timeline item is computed based on the previous and next items. |
||||
*/ |
||||
internal class TimelineItemsCacheInvalidator : DiffCacheInvalidator<TimelineItem> { |
||||
|
||||
private val delegate = DefaultDiffCacheInvalidator<TimelineItem>() |
||||
|
||||
override fun onChanged(position: Int, count: Int, cache: MutableDiffCache<TimelineItem>) { |
||||
delegate.onChanged(position, count, cache) |
||||
} |
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache<TimelineItem>) { |
||||
delegate.onMoved(fromPosition, toPosition, cache) |
||||
} |
||||
|
||||
override fun onInserted(position: Int, count: Int, cache: MutableDiffCache<TimelineItem>) { |
||||
cache.invalidateAround(position) |
||||
delegate.onInserted(position, count, cache) |
||||
} |
||||
|
||||
override fun onRemoved(position: Int, count: Int, cache: MutableDiffCache<TimelineItem>) { |
||||
cache.invalidateAround(position) |
||||
delegate.onRemoved(position, count, cache) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Invalidate the cache around the given position. |
||||
* It invalidates the previous and next items. |
||||
*/ |
||||
private fun MutableDiffCache<*>.invalidateAround(position: Int) { |
||||
if (position > 0) { |
||||
set(position - 1, null) |
||||
} |
||||
if (position < indices().last) { |
||||
set(position + 1, null) |
||||
} |
||||
} |
@ -0,0 +1,67 @@
@@ -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 @@
@@ -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 @@
@@ -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…
Reference in new issue