1 /*
2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.lazy.layout
18
19 import androidx.collection.mutableScatterMapOf
20 import androidx.compose.foundation.ExperimentalFoundationApi
21 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.DisposableEffect
23 import androidx.compose.runtime.Stable
24 import androidx.compose.runtime.saveable.SaveableStateHolder
25 import kotlin.jvm.JvmInline
26
27 /**
28 * This class:
29 * 1) Caches the lambdas being produced by [itemProvider]. This allows us to perform less
30 * recompositions as the compose runtime can skip the whole composition if we subcompose with the
31 * same instance of the content lambda.
32 * 2) Updates the mapping between keys and indexes when we have a new factory
33 * 3) Adds state restoration on top of the composable returned by [itemProvider] with help of
34 * [saveableStateHolder].
35 */
36 @OptIn(ExperimentalFoundationApi::class)
37 internal class LazyLayoutItemContentFactory(
38 private val saveableStateHolder: SaveableStateHolder,
39 val itemProvider: () -> LazyLayoutItemProvider,
40 ) {
41 /** Contains the cached lambdas produced by the [itemProvider]. */
42 private val lambdasCache = mutableScatterMapOf<Any, CachedItemContent>()
43
44 /**
45 * Returns the content type for the item with the given key. It is used to improve the item
46 * compositions reusing efficiency.
47 */
getContentTypenull48 fun getContentType(key: Any?): Any? {
49 if (key == null) return null
50
51 val cachedContent = lambdasCache[key]
52 return if (cachedContent != null) {
53 cachedContent.contentType
54 } else {
55 val itemProvider = itemProvider()
56 val index = itemProvider.getIndex(key)
57 if (index != -1) {
58 itemProvider.getContentType(index)
59 } else {
60 null
61 }
62 }
63 }
64
65 /** Return cached item content lambda or creates a new lambda and puts it in the cache. */
getContentnull66 fun getContent(index: Int, key: Any, contentType: Any?): @Composable () -> Unit {
67 val cached = lambdasCache[key]
68 return if (cached != null && cached.index == index && cached.contentType == contentType) {
69 cached.content
70 } else {
71 val newContent = CachedItemContent(index, key, contentType)
72 lambdasCache[key] = newContent
73 newContent.content
74 }
75 }
76
77 private inner class CachedItemContent(index: Int, val key: Any, val contentType: Any?) {
78 // the index resolved during the latest composition
79 var index = index
80 private set
81
82 private var _content: (@Composable () -> Unit)? = null
83 val content: (@Composable () -> Unit)
<lambda>null84 get() = _content ?: createContentLambda().also { _content = it }
85
createContentLambdanull86 private fun createContentLambda() =
87 @Composable {
88 val itemProvider = itemProvider()
89
90 var index = index
91 if (index >= itemProvider.itemCount || itemProvider.getKey(index) != key) {
92 index = itemProvider.getIndex(key)
93 if (index != -1) this.index = index
94 }
95
96 if (index != -1) {
97 SkippableItem(
98 itemProvider,
99 StableValue(saveableStateHolder),
100 index,
101 StableValue(key)
102 )
103 }
104 DisposableEffect(key) {
105 onDispose {
106 // we clear the cached content lambda when disposed to not leak
107 // RecomposeScopes
108 _content = null
109 }
110 }
111 }
112 }
113 }
114
115 @Stable @JvmInline private value class StableValue<T>(val value: T)
116
117 /**
118 * Hack around skippable functions to force skip SaveableStateProvider and Item block when nothing
119 * changed. It allows us to skip heavy-weight composition local providers.
120 */
121 @OptIn(ExperimentalFoundationApi::class)
122 @Composable
SkippableItemnull123 private fun SkippableItem(
124 itemProvider: LazyLayoutItemProvider,
125 saveableStateHolder: StableValue<SaveableStateHolder>,
126 index: Int,
127 key: StableValue<Any>
128 ) {
129 saveableStateHolder.value.SaveableStateProvider(key.value) {
130 itemProvider.Item(index, key.value)
131 }
132 }
133