Use snapshotFlow inside a LaunchedEffect, improve pre-fetching strategy
This commit is contained in:
parent
f95a959ed1
commit
784f00feb8
2 changed files with 32 additions and 19 deletions
|
|
@ -76,11 +76,16 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineView(
|
fun TimelineView(
|
||||||
|
|
@ -139,6 +144,10 @@ fun TimelineView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun prefetchMoreItems() {
|
||||||
|
state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||||
|
}
|
||||||
|
|
||||||
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
|
|
@ -185,9 +194,10 @@ fun TimelineView(
|
||||||
onClearFocusRequestState = ::clearFocusRequestState
|
onClearFocusRequestState = ::clearFocusRequestState
|
||||||
)
|
)
|
||||||
|
|
||||||
TimelinePrefetchingHelper(lazyListState = lazyListState) {
|
TimelinePrefetchingHelper(
|
||||||
state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
lazyListState = lazyListState,
|
||||||
}
|
prefetch = ::prefetchMoreItems
|
||||||
|
)
|
||||||
|
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
hasAnyEvent = state.hasAnyEvent,
|
hasAnyEvent = state.hasAnyEvent,
|
||||||
|
|
@ -217,7 +227,7 @@ private fun MessageShieldDialog(state: TimelineState) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimelinePrefetchingHelper(
|
private fun TimelinePrefetchingHelper(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
|
|
@ -225,25 +235,25 @@ private fun TimelinePrefetchingHelper(
|
||||||
) {
|
) {
|
||||||
val latestPrefetch by rememberUpdatedState(prefetch)
|
val latestPrefetch by rememberUpdatedState(prefetch)
|
||||||
|
|
||||||
// We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough
|
LaunchedEffect(Unit) {
|
||||||
val firstVisibleItemIndexFlow = snapshotFlow {
|
// We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough
|
||||||
lazyListState.firstVisibleItemIndex
|
val firstVisibleItemIndexFlow = snapshotFlow { lazyListState.firstVisibleItemIndex }
|
||||||
}
|
val layoutInfoFlow = snapshotFlow { lazyListState.layoutInfo }
|
||||||
val layoutInfoFlow = snapshotFlow {
|
val isScrollingFlow = snapshotFlow { lazyListState.isScrollInProgress }
|
||||||
lazyListState.layoutInfo
|
// This value changes too frequently, so we debounce it to avoid unnecessary prefetching. It's the equivalent of a conditional 'throttleLatest'
|
||||||
}
|
.conflate()
|
||||||
val isScrollingFlow = snapshotFlow {
|
.transform { isScrolling ->
|
||||||
lazyListState.isScrollInProgress
|
emit(isScrolling)
|
||||||
}
|
if (isScrolling) delay(100.milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(latestPrefetch) {
|
|
||||||
val isCloseToStartOfLoadedTimelineFlow = combine(layoutInfoFlow, firstVisibleItemIndexFlow) { layoutInfo, firstVisibleItemIndex ->
|
val isCloseToStartOfLoadedTimelineFlow = combine(layoutInfoFlow, firstVisibleItemIndexFlow) { layoutInfo, firstVisibleItemIndex ->
|
||||||
firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40
|
firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40
|
||||||
}
|
}
|
||||||
|
|
||||||
combine(
|
combine(
|
||||||
isCloseToStartOfLoadedTimelineFlow,
|
isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(),
|
||||||
isScrollingFlow,
|
isScrollingFlow.distinctUntilChanged(),
|
||||||
) { needsPrefetch, isScrolling ->
|
) { needsPrefetch, isScrolling ->
|
||||||
needsPrefetch && isScrolling
|
needsPrefetch && isScrolling
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ class TimelineViewTest {
|
||||||
@Test
|
@Test
|
||||||
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() {
|
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() {
|
||||||
val eventsRecorder = EventsRecorder<TimelineEvents>()
|
val eventsRecorder = EventsRecorder<TimelineEvents>()
|
||||||
val items = List<TimelineItem>(20) {
|
val items = List<TimelineItem>(200) {
|
||||||
aTimelineItemEvent(
|
aTimelineItemEvent(
|
||||||
eventId = EventId("\$event_$it"),
|
eventId = EventId("\$event_$it"),
|
||||||
content = aTimelineItemUnknownContent(),
|
content = aTimelineItemUnknownContent(),
|
||||||
|
|
@ -158,7 +158,10 @@ class TimelineViewTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
rule.onNodeWithTag("timeline").performScrollToIndex(10)
|
rule.onNodeWithTag("timeline").performScrollToIndex(180)
|
||||||
|
|
||||||
|
rule.mainClock.advanceTimeBy(1000)
|
||||||
|
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
TimelineEvents.OnScrollFinished(firstIndex = 0),
|
TimelineEvents.OnScrollFinished(firstIndex = 0),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue