Skip to content

Commit

Permalink
Extract an interface for LoadingStrategy (#2217)
Browse files Browse the repository at this point in the history
* Extract an interface for LoadingStrategy

* apiDump
  • Loading branch information
squarejesse authored Jul 31, 2024
1 parent ce03362 commit 63f55f8
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 146 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
35 changes: 24 additions & 11 deletions redwood-lazylayout-compose/api/redwood-lazylayout-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public final fun getDefaultPreloadItemCount ()I
public final fun getFirstIndex ()I
public final fun getLastIndex ()I
public final fun getPrimaryPreloadItemCount ()I
public fun <init> (Lapp/cash/redwood/lazylayout/compose/LoadingStrategy;)V
public synthetic fun <init> (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 <init> ()V
public fun <init> (IIIIZ)V
public synthetic fun <init> (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
}

50 changes: 31 additions & 19 deletions redwood-lazylayout-compose/api/redwood-lazylayout-compose.klib.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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<LazyListState, *> = Saver(
save = { it.firstIndex },
save = { it.strategy.firstIndex },
restore = {
LazyListState().apply {
programmaticScroll(firstIndex = it, animated = false, clobberUserScroll = false)
Expand All @@ -56,7 +50,9 @@ private val saver: Saver<LazyListState, *> = 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.
Expand All @@ -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,
Expand All @@ -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. */
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 63f55f8

Please sign in to comment.