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