diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b1f68efc..b87cd1b41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,11 @@ 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: - In Treehouse, events from the UI are now serialized on a background thread. This means that there is both a delay and a thread change between when a UI binding sends an event and when that object is converted to JSON. All arguments to events must not be mutable and support property reads on any thread. Best practice is for all event arguments to be completely immutable. +- `ProtocolFactory` interface is now sealed as arbitrary subtypes were never supported. Only schema-generated subtypes should be used. Fixed: - Nothing yet! diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9caec3b3a7..aafb7dd375 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -85,7 +85,7 @@ coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version turbine = "app.cash.turbine:turbine:1.1.0" ktlint = "com.pinterest.ktlint:ktlint-cli:1.3.1" ktlintComposeRules = "io.nlopez.compose.rules:ktlint:0.4.8" -googleJavaFormat = "com.google.googlejavaformat:google-java-format:1.22.0" +googleJavaFormat = "com.google.googlejavaformat:google-java-format:1.23.0" poko-gradlePlugin = "dev.drewhamilton.poko:poko-gradle-plugin:0.16.0" lint-core = { module = "com.android.tools.lint:lint", version.ref = "lint" } 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") }, diff --git a/redwood-protocol-host/api/redwood-protocol-host.klib.api b/redwood-protocol-host/api/redwood-protocol-host.klib.api index 656e626051..15b55755cb 100644 --- a/redwood-protocol-host/api/redwood-protocol-host.klib.api +++ b/redwood-protocol-host/api/redwood-protocol-host.klib.api @@ -16,11 +16,6 @@ abstract interface <#A: kotlin/Any> app.cash.redwood.protocol.host/GeneratedProt abstract fun widgetChildren(app.cash.redwood.protocol/WidgetTag): kotlin.collections/List // app.cash.redwood.protocol.host/GeneratedProtocolFactory.widgetChildren|widgetChildren(app.cash.redwood.protocol.WidgetTag){}[0] } -abstract interface <#A: kotlin/Any> app.cash.redwood.protocol.host/ProtocolFactory { // app.cash.redwood.protocol.host/ProtocolFactory|null[0] - abstract val widgetSystem // app.cash.redwood.protocol.host/ProtocolFactory.widgetSystem|{}widgetSystem[0] - abstract fun (): app.cash.redwood.widget/WidgetSystem<#A> // app.cash.redwood.protocol.host/ProtocolFactory.widgetSystem.|(){}[0] -} - abstract interface app.cash.redwood.protocol.host/ProtocolMismatchHandler { // app.cash.redwood.protocol.host/ProtocolMismatchHandler|null[0] abstract fun onUnknownChildren(app.cash.redwood.protocol/WidgetTag, app.cash.redwood.protocol/ChildrenTag) // app.cash.redwood.protocol.host/ProtocolMismatchHandler.onUnknownChildren|onUnknownChildren(app.cash.redwood.protocol.WidgetTag;app.cash.redwood.protocol.ChildrenTag){}[0] abstract fun onUnknownModifier(app.cash.redwood.protocol/ModifierTag) // app.cash.redwood.protocol.host/ProtocolMismatchHandler.onUnknownModifier|onUnknownModifier(app.cash.redwood.protocol.ModifierTag){}[0] @@ -33,6 +28,11 @@ abstract interface app.cash.redwood.protocol.host/ProtocolMismatchHandler { // a } } +sealed interface <#A: kotlin/Any> app.cash.redwood.protocol.host/ProtocolFactory { // app.cash.redwood.protocol.host/ProtocolFactory|null[0] + abstract val widgetSystem // app.cash.redwood.protocol.host/ProtocolFactory.widgetSystem|{}widgetSystem[0] + abstract fun (): app.cash.redwood.widget/WidgetSystem<#A> // app.cash.redwood.protocol.host/ProtocolFactory.widgetSystem.|(){}[0] +} + abstract class <#A: kotlin/Any> app.cash.redwood.protocol.host/ProtocolNode { // app.cash.redwood.protocol.host/ProtocolNode|null[0] constructor (app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag) // app.cash.redwood.protocol.host/ProtocolNode.|(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0] diff --git a/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolFactory.kt b/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolFactory.kt index a5af9bb50a..feb605ee0c 100644 --- a/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolFactory.kt +++ b/redwood-protocol-host/src/commonMain/kotlin/app/cash/redwood/protocol/host/ProtocolFactory.kt @@ -30,7 +30,7 @@ import kotlin.native.ObjCName * @see HostProtocolAdapter */ @ObjCName("ProtocolFactory", exact = true) -public interface ProtocolFactory { +public sealed interface ProtocolFactory { public val widgetSystem: WidgetSystem }