diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5fbd1750..d4c380a38d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ New: - Source-based schema parser is now the default. Can be disabled in your schema module with `redwood { useFir = false }`. +- Introduce a `LoadingStrategy` interface to manage `LazyList` preloading. Changed: - Nothing yet! diff --git a/redwood-lazylayout-compose/api/redwood-lazylayout-compose.api b/redwood-lazylayout-compose/api/redwood-lazylayout-compose.api index f78e97fbb2..39631244a9 100644 --- a/redwood-lazylayout-compose/api/redwood-lazylayout-compose.api +++ b/redwood-lazylayout-compose/api/redwood-lazylayout-compose.api @@ -24,28 +24,41 @@ public abstract interface class app/cash/redwood/lazylayout/compose/LazyListScop public class app/cash/redwood/lazylayout/compose/LazyListState { public static final field $stable I public fun ()V - public final fun getDefaultPreloadItemCount ()I - public final fun getFirstIndex ()I - public final fun getLastIndex ()I - public final fun getPrimaryPreloadItemCount ()I + public fun (Lapp/cash/redwood/lazylayout/compose/LoadingStrategy;)V + public synthetic fun (Lapp/cash/redwood/lazylayout/compose/LoadingStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getProgrammaticScrollIndex ()Lapp/cash/redwood/lazylayout/api/ScrollItemIndex; - public final fun getScrollInProgressPreloadItemCount ()I - public final fun getSecondaryPreloadItemCount ()I + public final fun getStrategy ()Lapp/cash/redwood/lazylayout/compose/LoadingStrategy; public final fun loadRange (I)Lkotlin/ranges/IntRange; public final fun onUserScroll (II)V public final fun programmaticScroll (IZZ)V public static synthetic fun programmaticScroll$default (Lapp/cash/redwood/lazylayout/compose/LazyListState;IZZILjava/lang/Object;)V - public final fun setDefaultPreloadItemCount (I)V - public final fun setPrimaryPreloadItemCount (I)V - public final fun setScrollInProgressPreloadItemCount (I)V - public final fun setSecondaryPreloadItemCount (I)V } public final class app/cash/redwood/lazylayout/compose/LazyListStateKt { - public static final fun rememberLazyListState (Landroidx/compose/runtime/Composer;I)Lapp/cash/redwood/lazylayout/compose/LazyListState; + public static final fun rememberLazyListState (Lapp/cash/redwood/lazylayout/compose/LoadingStrategy;Landroidx/compose/runtime/Composer;II)Lapp/cash/redwood/lazylayout/compose/LazyListState; +} + +public abstract interface class app/cash/redwood/lazylayout/compose/LoadingStrategy { + public abstract fun getFirstIndex ()I + public abstract fun loadRange (I)Lkotlin/ranges/IntRange; + public abstract fun onUserScroll (II)V + public abstract fun scrollTo (I)V } public final class app/cash/redwood/lazylayout/compose/RefreshableLazyListKt { public static final fun RefreshableLazyList-usczW4I (ZLkotlin/jvm/functions/Function2;IIZLkotlin/jvm/functions/Function0;IILapp/cash/redwood/ui/Margin;ILapp/cash/redwood/lazylayout/api/ScrollItemIndex;ILapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } +public final class app/cash/redwood/lazylayout/compose/ScrollOptimizedLoadingStrategy : app/cash/redwood/lazylayout/compose/LoadingStrategy { + public static final field $stable I + public fun ()V + public fun (IIIIZ)V + public synthetic fun (IIIIZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getFirstIndex ()I + public final fun getLastIndex ()I + public final fun getProgrammaticScrollIndex ()Lapp/cash/redwood/lazylayout/api/ScrollItemIndex; + public fun loadRange (I)Lkotlin/ranges/IntRange; + public fun onUserScroll (II)V + public fun scrollTo (I)V +} + diff --git a/redwood-lazylayout-compose/api/redwood-lazylayout-compose.klib.api b/redwood-lazylayout-compose/api/redwood-lazylayout-compose.klib.api index d29619e0b8..e57559a386 100644 --- a/redwood-lazylayout-compose/api/redwood-lazylayout-compose.klib.api +++ b/redwood-lazylayout-compose/api/redwood-lazylayout-compose.klib.api @@ -15,27 +15,38 @@ abstract interface app.cash.redwood.lazylayout.compose/LazyListScope { // app.ca abstract fun items(kotlin/Int, kotlin/Function3) // app.cash.redwood.lazylayout.compose/LazyListScope.items|items(kotlin.Int;kotlin.Function3){}[0] } +abstract interface app.cash.redwood.lazylayout.compose/LoadingStrategy { // app.cash.redwood.lazylayout.compose/LoadingStrategy|null[0] + abstract val firstIndex // app.cash.redwood.lazylayout.compose/LoadingStrategy.firstIndex|{}firstIndex[0] + abstract fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LoadingStrategy.firstIndex.|(){}[0] + + abstract fun loadRange(kotlin/Int): kotlin.ranges/IntRange // app.cash.redwood.lazylayout.compose/LoadingStrategy.loadRange|loadRange(kotlin.Int){}[0] + abstract fun onUserScroll(kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LoadingStrategy.onUserScroll|onUserScroll(kotlin.Int;kotlin.Int){}[0] + abstract fun scrollTo(kotlin/Int) // app.cash.redwood.lazylayout.compose/LoadingStrategy.scrollTo|scrollTo(kotlin.Int){}[0] +} + +final class app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy : app.cash.redwood.lazylayout.compose/LoadingStrategy { // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy|null[0] + constructor (kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Boolean = ...) // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.|(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Boolean){}[0] + + final var firstIndex // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.firstIndex|{}firstIndex[0] + final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.firstIndex.|(){}[0] + final var lastIndex // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.lastIndex|{}lastIndex[0] + final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.lastIndex.|(){}[0] + final var programmaticScrollIndex // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.programmaticScrollIndex|{}programmaticScrollIndex[0] + final fun (): app.cash.redwood.lazylayout.api/ScrollItemIndex // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.programmaticScrollIndex.|(){}[0] + + final fun loadRange(kotlin/Int): kotlin.ranges/IntRange // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.loadRange|loadRange(kotlin.Int){}[0] + final fun onUserScroll(kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.onUserScroll|onUserScroll(kotlin.Int;kotlin.Int){}[0] + final fun scrollTo(kotlin/Int) // app.cash.redwood.lazylayout.compose/ScrollOptimizedLoadingStrategy.scrollTo|scrollTo(kotlin.Int){}[0] +} + open class app.cash.redwood.lazylayout.compose/LazyListState { // app.cash.redwood.lazylayout.compose/LazyListState|null[0] - constructor () // app.cash.redwood.lazylayout.compose/LazyListState.|(){}[0] - - final var defaultPreloadItemCount // app.cash.redwood.lazylayout.compose/LazyListState.defaultPreloadItemCount|{}defaultPreloadItemCount[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.defaultPreloadItemCount.|(){}[0] - final fun (kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyListState.defaultPreloadItemCount.|(kotlin.Int){}[0] - final var firstIndex // app.cash.redwood.lazylayout.compose/LazyListState.firstIndex|{}firstIndex[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.firstIndex.|(){}[0] - final var lastIndex // app.cash.redwood.lazylayout.compose/LazyListState.lastIndex|{}lastIndex[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.lastIndex.|(){}[0] - final var primaryPreloadItemCount // app.cash.redwood.lazylayout.compose/LazyListState.primaryPreloadItemCount|{}primaryPreloadItemCount[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.primaryPreloadItemCount.|(){}[0] - final fun (kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyListState.primaryPreloadItemCount.|(kotlin.Int){}[0] + constructor (app.cash.redwood.lazylayout.compose/LoadingStrategy = ...) // app.cash.redwood.lazylayout.compose/LazyListState.|(app.cash.redwood.lazylayout.compose.LoadingStrategy){}[0] + + final val strategy // app.cash.redwood.lazylayout.compose/LazyListState.strategy|{}strategy[0] + final fun (): app.cash.redwood.lazylayout.compose/LoadingStrategy // app.cash.redwood.lazylayout.compose/LazyListState.strategy.|(){}[0] + final var programmaticScrollIndex // app.cash.redwood.lazylayout.compose/LazyListState.programmaticScrollIndex|{}programmaticScrollIndex[0] final fun (): app.cash.redwood.lazylayout.api/ScrollItemIndex // app.cash.redwood.lazylayout.compose/LazyListState.programmaticScrollIndex.|(){}[0] - final var scrollInProgressPreloadItemCount // app.cash.redwood.lazylayout.compose/LazyListState.scrollInProgressPreloadItemCount|{}scrollInProgressPreloadItemCount[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.scrollInProgressPreloadItemCount.|(){}[0] - final fun (kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyListState.scrollInProgressPreloadItemCount.|(kotlin.Int){}[0] - final var secondaryPreloadItemCount // app.cash.redwood.lazylayout.compose/LazyListState.secondaryPreloadItemCount|{}secondaryPreloadItemCount[0] - final fun (): kotlin/Int // app.cash.redwood.lazylayout.compose/LazyListState.secondaryPreloadItemCount.|(){}[0] - final fun (kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyListState.secondaryPreloadItemCount.|(kotlin.Int){}[0] final fun loadRange(kotlin/Int): kotlin.ranges/IntRange // app.cash.redwood.lazylayout.compose/LazyListState.loadRange|loadRange(kotlin.Int){}[0] final fun onUserScroll(kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyListState.onUserScroll|onUserScroll(kotlin.Int;kotlin.Int){}[0] @@ -48,6 +59,7 @@ final val app.cash.redwood.lazylayout.compose.layout/app_cash_redwood_lazylayout final val app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListInterval$stableprop // app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListInterval$stableprop|#static{}app_cash_redwood_lazylayout_compose_LazyListInterval$stableprop[0] final val app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListIntervalContent$stableprop // app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListIntervalContent$stableprop|#static{}app_cash_redwood_lazylayout_compose_LazyListIntervalContent$stableprop[0] final val app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListState$stableprop // app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_LazyListState$stableprop|#static{}app_cash_redwood_lazylayout_compose_LazyListState$stableprop[0] +final val app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_ScrollOptimizedLoadingStrategy$stableprop // app.cash.redwood.lazylayout.compose/app_cash_redwood_lazylayout_compose_ScrollOptimizedLoadingStrategy$stableprop|#static{}app_cash_redwood_lazylayout_compose_ScrollOptimizedLoadingStrategy$stableprop[0] final fun app.cash.redwood.lazylayout.compose/LazyColumn(kotlin/Boolean, kotlin/Function0?, kotlin/Function2, app.cash.redwood/Modifier?, app.cash.redwood.lazylayout.compose/LazyListState?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/UInt, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyColumn|LazyColumn(kotlin.Boolean;kotlin.Function0?;kotlin.Function2;app.cash.redwood.Modifier?;app.cash.redwood.lazylayout.compose.LazyListState?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.UInt;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun app.cash.redwood.lazylayout.compose/LazyColumn(kotlin/Function2, app.cash.redwood/Modifier?, app.cash.redwood.lazylayout.compose/LazyListState?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyColumn|LazyColumn(kotlin.Function2;app.cash.redwood.Modifier?;app.cash.redwood.lazylayout.compose.LazyListState?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] @@ -55,7 +67,7 @@ final fun app.cash.redwood.lazylayout.compose/LazyList(kotlin/Boolean, kotlin/Fu final fun app.cash.redwood.lazylayout.compose/LazyRow(kotlin/Boolean, kotlin/Function0?, kotlin/Function2, app.cash.redwood/Modifier?, app.cash.redwood.lazylayout.compose/LazyListState?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/UInt, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyRow|LazyRow(kotlin.Boolean;kotlin.Function0?;kotlin.Function2;app.cash.redwood.Modifier?;app.cash.redwood.lazylayout.compose.LazyListState?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.UInt;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun app.cash.redwood.lazylayout.compose/LazyRow(kotlin/Function2, app.cash.redwood/Modifier?, app.cash.redwood.lazylayout.compose/LazyListState?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/LazyRow|LazyRow(kotlin.Function2;app.cash.redwood.Modifier?;app.cash.redwood.lazylayout.compose.LazyListState?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun app.cash.redwood.lazylayout.compose/RefreshableLazyList(kotlin/Boolean, kotlin/Function2, kotlin/Int, kotlin/Int, kotlin/Boolean, kotlin/Function0?, app.cash.redwood.layout.api/Constraint, app.cash.redwood.layout.api/Constraint, app.cash.redwood.ui/Margin, app.cash.redwood.layout.api/CrossAxisAlignment, app.cash.redwood.lazylayout.api/ScrollItemIndex, kotlin/UInt, app.cash.redwood/Modifier?, kotlin/Function2, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.lazylayout.compose/RefreshableLazyList|RefreshableLazyList(kotlin.Boolean;kotlin.Function2;kotlin.Int;kotlin.Int;kotlin.Boolean;kotlin.Function0?;app.cash.redwood.layout.api.Constraint;app.cash.redwood.layout.api.Constraint;app.cash.redwood.ui.Margin;app.cash.redwood.layout.api.CrossAxisAlignment;app.cash.redwood.lazylayout.api.ScrollItemIndex;kotlin.UInt;app.cash.redwood.Modifier?;kotlin.Function2;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -final fun app.cash.redwood.lazylayout.compose/rememberLazyListState(androidx.compose.runtime/Composer?, kotlin/Int): app.cash.redwood.lazylayout.compose/LazyListState // app.cash.redwood.lazylayout.compose/rememberLazyListState|rememberLazyListState(androidx.compose.runtime.Composer?;kotlin.Int){}[0] +final fun app.cash.redwood.lazylayout.compose/rememberLazyListState(app.cash.redwood.lazylayout.compose/LoadingStrategy?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): app.cash.redwood.lazylayout.compose/LazyListState // app.cash.redwood.lazylayout.compose/rememberLazyListState|rememberLazyListState(app.cash.redwood.lazylayout.compose.LoadingStrategy?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final inline fun <#A: kotlin/Any?> (app.cash.redwood.lazylayout.compose/LazyListScope).app.cash.redwood.lazylayout.compose/items(kotlin.collections/List<#A>, crossinline kotlin/Function3<#A, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // app.cash.redwood.lazylayout.compose/items|items@app.cash.redwood.lazylayout.compose.LazyListScope(kotlin.collections.List<0:0>;kotlin.Function3<0:0,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){0§}[0] final inline fun <#A: kotlin/Any?> (app.cash.redwood.lazylayout.compose/LazyListScope).app.cash.redwood.lazylayout.compose/items(kotlin/Array<#A>, crossinline kotlin/Function3<#A, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // app.cash.redwood.lazylayout.compose/items|items@app.cash.redwood.lazylayout.compose.LazyListScope(kotlin.Array<0:0>;kotlin.Function3<0:0,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){0§}[0] final inline fun <#A: kotlin/Any?> (app.cash.redwood.lazylayout.compose/LazyListScope).app.cash.redwood.lazylayout.compose/itemsIndexed(kotlin.collections/List<#A>, crossinline kotlin/Function4) // app.cash.redwood.lazylayout.compose/itemsIndexed|itemsIndexed@app.cash.redwood.lazylayout.compose.LazyListScope(kotlin.collections.List<0:0>;kotlin.Function4){0§}[0] diff --git a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyListState.kt b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyListState.kt index 10aa79926a..08014bec30 100644 --- a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyListState.kt +++ b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyListState.kt @@ -17,33 +17,27 @@ package app.cash.redwood.lazylayout.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import app.cash.redwood.lazylayout.api.ScrollItemIndex -private const val DEFAULT_PRELOAD_ITEM_COUNT = 15 -private const val SCROLL_IN_PROGRESS_PRELOAD_ITEM_COUNT = 5 -private const val PRIMARY_PRELOAD_ITEM_COUNT = 20 -private const val SECONDARY_PRELOAD_ITEM_COUNT = 10 - -private const val DEFAULT_SCROLL_INDEX = -1 - /** * Creates a [LazyListState] that is remembered across compositions. */ @Composable -public fun rememberLazyListState(): LazyListState { +public fun rememberLazyListState( + strategy: LoadingStrategy = ScrollOptimizedLoadingStrategy(), +): LazyListState { return rememberSaveable(saver = saver) { - LazyListState() + LazyListState(strategy) } } /** The default [Saver] implementation for [LazyListState]. */ private val saver: Saver = Saver( - save = { it.firstIndex }, + save = { it.strategy.firstIndex }, restore = { LazyListState().apply { programmaticScroll(firstIndex = it, animated = false, clobberUserScroll = false) @@ -56,7 +50,9 @@ private val saver: Saver = Saver( * * In most cases, this will be created via [rememberLazyListState]. */ -public open class LazyListState { +public open class LazyListState( + public val strategy: LoadingStrategy = ScrollOptimizedLoadingStrategy(), +) { /** * Update this to trigger a programmatic scroll. This may be updated multiple times, including * when the previous scroll state is restored. @@ -69,26 +65,6 @@ public open class LazyListState { /** Once we receive a user scroll, we limit which programmatic scrolls we apply. */ private var userScrolled = false - /** Bounds of what the user is looking at. Everything else is placeholders! */ - public var firstIndex: Int by mutableIntStateOf(0) - private set - public var lastIndex: Int by mutableIntStateOf(0) - private set - - internal var preloadItems: Boolean = true - - public var defaultPreloadItemCount: Int = DEFAULT_PRELOAD_ITEM_COUNT - public var scrollInProgressPreloadItemCount: Int = SCROLL_IN_PROGRESS_PRELOAD_ITEM_COUNT - public var primaryPreloadItemCount: Int = PRIMARY_PRELOAD_ITEM_COUNT - public var secondaryPreloadItemCount: Int = SECONDARY_PRELOAD_ITEM_COUNT - - private var firstIndexFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) - private var firstIndexFromPrevious2: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) - private var lastIndexFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) - - private var beginFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) - private var endFromPrevious1: Int by mutableStateOf(DEFAULT_SCROLL_INDEX) - /** Perform a programmatic scroll. */ public fun programmaticScroll( firstIndex: Int, @@ -98,16 +74,14 @@ public open class LazyListState { require(firstIndex >= 0) if (!clobberUserScroll && userScrolled) return + strategy.scrollTo(firstIndex) + val previous = programmaticScrollIndex this.programmaticScrollIndex = ScrollItemIndex( id = previous.id + 1, index = firstIndex, animated = animated, ) - - val delta = (lastIndex - this.firstIndex) - this.firstIndex = firstIndex - this.lastIndex = firstIndex + delta } /** React to a user-initiated scroll. */ @@ -116,85 +90,10 @@ public open class LazyListState { userScrolled = true } - this.firstIndex = firstIndex - this.lastIndex = lastIndex + strategy.onUserScroll(firstIndex, lastIndex) } public fun loadRange(itemCount: Int): IntRange { - // Ensure that the range includes `firstIndex` through `lastIndex`. - var begin = firstIndex - var end = lastIndex - - val isScrollingDown = firstIndexFromPrevious1 != DEFAULT_SCROLL_INDEX && firstIndexFromPrevious1 < firstIndex - val isScrollingUp = firstIndexFromPrevious1 != DEFAULT_SCROLL_INDEX && firstIndexFromPrevious1 > firstIndex - val hasStoppedScrolling = firstIndexFromPrevious2 != DEFAULT_SCROLL_INDEX && firstIndex == firstIndexFromPrevious1 - val wasScrollingDown = firstIndexFromPrevious1 > firstIndexFromPrevious2 - val wasScrollingUp = firstIndexFromPrevious1 < firstIndexFromPrevious2 - - // Expand the range depending on scroll direction. - when { - // Ignore preloads. - !preloadItems -> { - // No-op - } - - isScrollingDown -> { - begin -= scrollInProgressPreloadItemCount - end += primaryPreloadItemCount - } - - isScrollingUp -> { - begin -= primaryPreloadItemCount - end += scrollInProgressPreloadItemCount - } - - hasStoppedScrolling && wasScrollingDown -> { - begin -= secondaryPreloadItemCount - end += primaryPreloadItemCount - } - - hasStoppedScrolling && wasScrollingUp -> { - begin -= primaryPreloadItemCount - end += secondaryPreloadItemCount - } - - // New. - else -> { - end += defaultPreloadItemCount - } - } - - // On initial load, set lastIndex to the end of the loaded window. - if (lastIndex == 0) { - lastIndex = end - } - - // If we're contiguous with the previous visible window, - // don't rush to remove things from the previous range. - if (beginFromPrevious1 != DEFAULT_SCROLL_INDEX && - endFromPrevious1 != DEFAULT_SCROLL_INDEX - ) { - // Case one: Contiguous scroll down - if (begin in firstIndexFromPrevious1..lastIndexFromPrevious1) { - begin = beginFromPrevious1 - } - - // Case two: Contiguous scroll up - if (end in firstIndexFromPrevious1..lastIndexFromPrevious1) { - end = endFromPrevious1 - } - } - - begin = begin.coerceIn(0, itemCount) - end = end.coerceIn(0, itemCount) - - this.firstIndexFromPrevious2 = firstIndexFromPrevious1 - this.firstIndexFromPrevious1 = firstIndex - this.lastIndexFromPrevious1 = lastIndex - - this.beginFromPrevious1 = begin - this.endFromPrevious1 = end - - return begin until end + return strategy.loadRange(itemCount) } } diff --git a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LoadingStrategy.kt b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LoadingStrategy.kt new file mode 100644 index 0000000000..66e1dcd98d --- /dev/null +++ b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LoadingStrategy.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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 app.cash.redwood.lazylayout.compose + +public interface LoadingStrategy { + /** + * Returns the most recent first index scrolled to. This is used to save the scroll position when + * the view is unloaded. + */ + public val firstIndex: Int + + /** Perform a programmatic scroll to [firstIndex]. */ + public fun scrollTo(firstIndex: Int) + + /** React to a user-initiated scroll to the target range. */ + public fun onUserScroll(firstIndex: Int, lastIndex: Int) + + /** + * Returns the range of items to render into the view tree. This should be a slice of + * `0..(itemCount - 1)`. It should cover the most-recently scrolled to `firstIndex..lastIndex` + * range, plus any adjacent indexes to preload. + */ + public fun loadRange(itemCount: Int): IntRange +} diff --git a/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/ScrollOptimizedLoadingStrategy.kt b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/ScrollOptimizedLoadingStrategy.kt new file mode 100644 index 0000000000..5f0f2cf0e2 --- /dev/null +++ b/redwood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/ScrollOptimizedLoadingStrategy.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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 app.cash.redwood.lazylayout.compose + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import app.cash.redwood.lazylayout.api.ScrollItemIndex + +private const val DEFAULT_PRELOAD_ITEM_COUNT = 15 +private const val SCROLL_IN_PROGRESS_PRELOAD_ITEM_COUNT = 5 +private const val PRIMARY_PRELOAD_ITEM_COUNT = 20 +private const val SECONDARY_PRELOAD_ITEM_COUNT = 10 + +private const val DEFAULT_SCROLL_INDEX = -1 + +/** + * A loading strategy that preloads items above and below the visible range. + * + * When scrolling, this loads more items in the direction the user is scrolling to. + * + * The size of the loading window is kept small while scrolling. It grows when scrolling stops. + * + * This will retain already-loaded items that it wouldn't load otherwise. + */ +public class ScrollOptimizedLoadingStrategy( + private val defaultPreloadItemCount: Int = DEFAULT_PRELOAD_ITEM_COUNT, + private val scrollInProgressPreloadItemCount: Int = SCROLL_IN_PROGRESS_PRELOAD_ITEM_COUNT, + private val primaryPreloadItemCount: Int = PRIMARY_PRELOAD_ITEM_COUNT, + private val secondaryPreloadItemCount: Int = SECONDARY_PRELOAD_ITEM_COUNT, + private val preloadItems: Boolean = true, +) : LoadingStrategy { + /** + * Update this to trigger a programmatic scroll. This may be updated multiple times, including + * when the previous scroll state is restored. + */ + public var programmaticScrollIndex: ScrollItemIndex by mutableStateOf( + ScrollItemIndex(id = 0, index = 0, animated = false), + ) + private set + + /** Bounds of what the user is looking at. Everything else is placeholders! */ + public override var firstIndex: Int by mutableIntStateOf(0) + private set + public var lastIndex: Int by mutableIntStateOf(0) + private set + + private var firstIndexFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) + private var firstIndexFromPrevious2: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) + private var lastIndexFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) + + private var beginFromPrevious1: Int by mutableIntStateOf(DEFAULT_SCROLL_INDEX) + private var endFromPrevious1: Int by mutableStateOf(DEFAULT_SCROLL_INDEX) + + override fun scrollTo(firstIndex: Int) { + require(firstIndex >= 0) + + val delta = (lastIndex - this.firstIndex) + this.firstIndex = firstIndex + this.lastIndex = firstIndex + delta + } + + override fun onUserScroll(firstIndex: Int, lastIndex: Int) { + this.firstIndex = firstIndex + this.lastIndex = lastIndex + } + + public override fun loadRange(itemCount: Int): IntRange { + // Ensure that the range includes `firstIndex` through `lastIndex`. + var begin = firstIndex + var end = lastIndex + + val isScrollingDown = firstIndexFromPrevious1 != DEFAULT_SCROLL_INDEX && firstIndexFromPrevious1 < firstIndex + val isScrollingUp = firstIndexFromPrevious1 != DEFAULT_SCROLL_INDEX && firstIndexFromPrevious1 > firstIndex + val hasStoppedScrolling = firstIndexFromPrevious2 != DEFAULT_SCROLL_INDEX && firstIndex == firstIndexFromPrevious1 + val wasScrollingDown = firstIndexFromPrevious1 > firstIndexFromPrevious2 + val wasScrollingUp = firstIndexFromPrevious1 < firstIndexFromPrevious2 + + // Expand the range depending on scroll direction. + when { + // Ignore preloads. + !preloadItems -> { + // No-op + } + + isScrollingDown -> { + begin -= scrollInProgressPreloadItemCount + end += primaryPreloadItemCount + } + + isScrollingUp -> { + begin -= primaryPreloadItemCount + end += scrollInProgressPreloadItemCount + } + + hasStoppedScrolling && wasScrollingDown -> { + begin -= secondaryPreloadItemCount + end += primaryPreloadItemCount + } + + hasStoppedScrolling && wasScrollingUp -> { + begin -= primaryPreloadItemCount + end += secondaryPreloadItemCount + } + + // New. + else -> { + end += defaultPreloadItemCount + } + } + + // On initial load, set lastIndex to the end of the loaded window. + if (lastIndex == 0) { + lastIndex = end + } + + // If we're contiguous with the previous visible window, + // don't rush to remove things from the previous range. + if (beginFromPrevious1 != DEFAULT_SCROLL_INDEX && + endFromPrevious1 != DEFAULT_SCROLL_INDEX + ) { + // Case one: Contiguous scroll down + if (begin in firstIndexFromPrevious1..lastIndexFromPrevious1) { + begin = beginFromPrevious1 + } + + // Case two: Contiguous scroll up + if (end in firstIndexFromPrevious1..lastIndexFromPrevious1) { + end = endFromPrevious1 + } + } + + begin = begin.coerceIn(0, itemCount) + end = end.coerceIn(0, itemCount) + + this.firstIndexFromPrevious2 = firstIndexFromPrevious1 + this.firstIndexFromPrevious1 = firstIndex + this.lastIndexFromPrevious1 = lastIndex + + this.beginFromPrevious1 = begin + this.endFromPrevious1 = end + + return begin until end + } +} diff --git a/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt b/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt index 7b3ec574d1..1c4502abf8 100644 --- a/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt +++ b/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt @@ -116,9 +116,9 @@ class LazyListTest { TestSchemaTester { var index5ComposeCount = 0 setContent { - val lazyListState = rememberLazyListState().apply { - preloadItems = false - } + val lazyListState = rememberLazyListState( + ScrollOptimizedLoadingStrategy(preloadItems = false), + ) LazyColumn( state = lazyListState, placeholder = { Text("Placeholder") },