Skip to content

Commit

Permalink
Merge branch 'trunk' into jw.defer-event-serialization.2024-07-30
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton authored Jul 31, 2024
2 parents 0779b75 + fa9e1f5 commit ff3d4be
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 153 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
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 ff3d4be

Please sign in to comment.