Skip to content

Commit badca69

Browse files
committed
Use snapshotFlow inside a LaunchedEffect, improve pre-fetching strategy
1 parent e1506d9 commit badca69

File tree

2 files changed

+32
-19
lines changed
  • features/messages/impl/src

2 files changed

+32
-19
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,16 @@ import io.element.android.libraries.testtags.TestTags
7676
import io.element.android.libraries.testtags.testTag
7777
import io.element.android.libraries.ui.strings.CommonStrings
7878
import kotlinx.coroutines.ExperimentalCoroutinesApi
79+
import kotlinx.coroutines.FlowPreview
80+
import kotlinx.coroutines.delay
7981
import kotlinx.coroutines.flow.collectLatest
8082
import kotlinx.coroutines.flow.combine
83+
import kotlinx.coroutines.flow.conflate
8184
import kotlinx.coroutines.flow.distinctUntilChanged
85+
import kotlinx.coroutines.flow.transform
8286
import kotlinx.coroutines.launch
8387
import timber.log.Timber
88+
import kotlin.time.Duration.Companion.milliseconds
8489

8590
@Composable
8691
fun TimelineView(
@@ -139,6 +144,10 @@ fun TimelineView(
139144
)
140145
}
141146

147+
fun prefetchMoreItems() {
148+
state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
149+
}
150+
142151
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
143152
AnimatedVisibility(visible = true, enter = fadeIn()) {
144153
Box(modifier) {
@@ -185,9 +194,10 @@ fun TimelineView(
185194
onClearFocusRequestState = ::clearFocusRequestState
186195
)
187196

188-
TimelinePrefetchingHelper(lazyListState = lazyListState) {
189-
state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
190-
}
197+
TimelinePrefetchingHelper(
198+
lazyListState = lazyListState,
199+
prefetch = ::prefetchMoreItems
200+
)
191201

192202
TimelineScrollHelper(
193203
hasAnyEvent = state.hasAnyEvent,
@@ -217,33 +227,33 @@ private fun MessageShieldDialog(state: TimelineState) {
217227
)
218228
}
219229

220-
@OptIn(ExperimentalCoroutinesApi::class)
230+
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
221231
@Composable
222232
private fun TimelinePrefetchingHelper(
223233
lazyListState: LazyListState,
224234
prefetch: () -> Unit,
225235
) {
226236
val latestPrefetch by rememberUpdatedState(prefetch)
227237

228-
// We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough
229-
val firstVisibleItemIndexFlow = snapshotFlow {
230-
lazyListState.firstVisibleItemIndex
231-
}
232-
val layoutInfoFlow = snapshotFlow {
233-
lazyListState.layoutInfo
234-
}
235-
val isScrollingFlow = snapshotFlow {
236-
lazyListState.isScrollInProgress
237-
}
238+
LaunchedEffect(Unit) {
239+
// We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough
240+
val firstVisibleItemIndexFlow = snapshotFlow { lazyListState.firstVisibleItemIndex }
241+
val layoutInfoFlow = snapshotFlow { lazyListState.layoutInfo }
242+
val isScrollingFlow = snapshotFlow { lazyListState.isScrollInProgress }
243+
// This value changes too frequently, so we debounce it to avoid unnecessary prefetching. It's the equivalent of a conditional 'throttleLatest'
244+
.conflate()
245+
.transform { isScrolling ->
246+
emit(isScrolling)
247+
if (isScrolling) delay(100.milliseconds)
248+
}
238249

239-
LaunchedEffect(latestPrefetch) {
240250
val isCloseToStartOfLoadedTimelineFlow = combine(layoutInfoFlow, firstVisibleItemIndexFlow) { layoutInfo, firstVisibleItemIndex ->
241251
firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40
242252
}
243253

244254
combine(
245-
isCloseToStartOfLoadedTimelineFlow,
246-
isScrollingFlow,
255+
isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(),
256+
isScrollingFlow.distinctUntilChanged(),
247257
) { needsPrefetch, isScrolling ->
248258
needsPrefetch && isScrolling
249259
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class TimelineViewTest {
142142
@Test
143143
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() {
144144
val eventsRecorder = EventsRecorder<TimelineEvents>()
145-
val items = List<TimelineItem>(20) {
145+
val items = List<TimelineItem>(200) {
146146
aTimelineItemEvent(
147147
eventId = EventId("\$event_$it"),
148148
content = aTimelineItemUnknownContent(),
@@ -158,7 +158,10 @@ class TimelineViewTest {
158158
),
159159
)
160160

161-
rule.onNodeWithTag("timeline").performScrollToIndex(10)
161+
rule.onNodeWithTag("timeline").performScrollToIndex(180)
162+
163+
rule.mainClock.advanceTimeBy(1000)
164+
162165
eventsRecorder.assertList(
163166
listOf(
164167
TimelineEvents.OnScrollFinished(firstIndex = 0),

0 commit comments

Comments
 (0)