LazyListItemContentFactory.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * 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 androidx.compose.foundation.lazy

import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
 * This class:
 * 1) Caches the lambdas being produced by [scopedFactory]. This allows us to perform less
 * recompositions as the compose runtime can skip the whole composition if we subcompose with the
 * same instance of the content lambda.
 * 2) Updates the mapping between keys and indexes when we have a new factory
 * 3) Creates an [itemScope] to be used with [scopedFactory]
 * 4) Adds state restoration on top of the composable returned by [scopedFactory] with help of
 * [saveableStateHolder].
 */
internal class LazyListItemContentFactory(
    private val saveableStateHolder: SaveableStateHolder,
    private var scopedFactory: LazyKeyAndScopedContentFactory,
    itemsCount: Int
) {

    /**
     * Contains the cached lambdas produced by the [scopedFactory].
     */
    private val lambdasCache = mutableMapOf<Any, CachedItemContent>()

    /**
     * Current factory for creating an item content lambdas.
     */
    private var observableScopedFactory by mutableStateOf(scopedFactory, neverEqualPolicy())

    /**
     * Current items count.
     */
    private var itemsCount: Int by mutableStateOf(itemsCount)

    fun update(
        scopedFactory: LazyKeyAndScopedContentFactory,
        itemsCount: Int,
        state: LazyListState
    ) {
        if (this.scopedFactory != scopedFactory) {
            this.scopedFactory = scopedFactory
            observableScopedFactory = scopedFactory
        }
        this.itemsCount = itemsCount
        if (itemsCount > 0) {
            val firstVisible = state.firstVisibleItemIndexNonObservable.value
            val lastVisible = state.lastVisibleItemIndexNonObservable.value
            for (i in firstVisible..minOf(itemsCount - 1, lastVisible)) {
                lambdasCache[scopedFactory.getKey(i)]?.index = i
            }
        }
    }

    /**
     * Return a key associated with the given [index].
     */
    fun getKey(index: Int) = scopedFactory.getKey(index)

    /**
     * Return cached item content lambda or creates a new lambda and puts it in the cache.
     */
    fun getContent(index: Int, key: Any): @Composable () -> Unit {
        val cachedContent = lambdasCache.getOrPut(key) { CachedItemContent(index, key) }
        cachedContent.index = index
        return cachedContent.content
    }

    private inner class CachedItemContent(
        initialIndex: Int,
        val key: Any
    ) {
        var index by mutableStateOf(initialIndex)

        val content: @Composable () -> Unit = @Composable {
            if (index < itemsCount) {
                val content = observableScopedFactory.getContent(index, itemScope)
                saveableStateHolder.SaveableStateProvider(key, content)
            }
        }
    }

    /**
     * The cached instance of the scope to be used for composing items.
     */
    private var itemScope by mutableStateOf(InitialLazyItemsScopeImpl)
    private var lastDensity: Density = Density(0f, 0f)
    private var lastConstraints: Constraints = Constraints()

    /**
     * Updates the [itemScope] with the last [constraints] we got from the parent.
     */
    fun updateItemScope(density: Density, constraints: Constraints) {
        if (lastDensity != density || lastConstraints != constraints) {
            lastDensity = density
            lastConstraints = constraints
            with(density) {
                val width = constraints.maxWidth.toDp()
                val height = constraints.maxHeight.toDp()
                itemScope = LazyItemScopeImpl(width, height)
            }
        }
    }
}

/**
 * Pre-allocated initial value for [LazyItemScopeImpl] to not have it nullable and avoid using
 * late init.
 */
private val InitialLazyItemsScopeImpl = LazyItemScopeImpl(0.dp, 0.dp)

private data class LazyItemScopeImpl(
    val maxWidth: Dp,
    val maxHeight: Dp
) : LazyItemScope {
    override fun Modifier.fillParentMaxSize(fraction: Float) = size(
        maxWidth * fraction,
        maxHeight * fraction
    )

    override fun Modifier.fillParentMaxWidth(fraction: Float) =
        width(maxWidth * fraction)

    override fun Modifier.fillParentMaxHeight(fraction: Float) =
        height(maxHeight * fraction)
}