1 /*
<lambda>null2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.paging
18
19 import androidx.kruth.assertThat
20 import androidx.paging.LoadState.Loading
21 import androidx.paging.LoadState.NotLoading
22 import androidx.paging.LoadType.PREPEND
23 import androidx.paging.PageEvent.Drop
24 import androidx.paging.PagingSource.LoadResult
25 import kotlin.coroutines.EmptyCoroutineContext
26 import kotlin.test.Test
27 import kotlin.test.assertEquals
28 import kotlin.test.assertFailsWith
29 import kotlin.test.assertFalse
30 import kotlin.test.assertNull
31 import kotlin.test.assertTrue
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.Job
34 import kotlinx.coroutines.async
35 import kotlinx.coroutines.cancelChildren
36 import kotlinx.coroutines.channels.Channel
37 import kotlinx.coroutines.delay
38 import kotlinx.coroutines.flow.collectLatest
39 import kotlinx.coroutines.flow.consumeAsFlow
40 import kotlinx.coroutines.flow.emptyFlow
41 import kotlinx.coroutines.flow.filterNotNull
42 import kotlinx.coroutines.flow.first
43 import kotlinx.coroutines.flow.flow
44 import kotlinx.coroutines.flow.flowOf
45 import kotlinx.coroutines.flow.map
46 import kotlinx.coroutines.launch
47 import kotlinx.coroutines.suspendCancellableCoroutine
48 import kotlinx.coroutines.test.TestScope
49 import kotlinx.coroutines.test.UnconfinedTestDispatcher
50 import kotlinx.coroutines.test.advanceTimeBy
51 import kotlinx.coroutines.test.advanceUntilIdle
52 import kotlinx.coroutines.test.runTest
53
54 /**
55 * run some tests with cached-in to ensure caching does not change behavior in the single consumer
56 * cases.
57 */
58 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalPagingApi::class)
59 class PagingDataPresenterTest {
60 private val testScope = TestScope(UnconfinedTestDispatcher())
61
62 @Test
63 fun collectFrom_static() =
64 testScope.runTest {
65 val presenter = SimplePresenter()
66 val receiver = UiReceiverFake()
67
68 val job1 = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
69 advanceUntilIdle()
70 job1.cancel()
71
72 val job2 = launch { presenter.collectFrom(PagingData.empty()) }
73 advanceUntilIdle()
74 job2.cancel()
75
76 // Static replacement should also replace the UiReceiver from previous generation.
77 presenter.retry()
78 presenter.refresh()
79 advanceUntilIdle()
80
81 assertFalse { receiver.retryEvents.isNotEmpty() }
82 assertFalse { receiver.refreshEvents.isNotEmpty() }
83 }
84
85 @Test
86 fun collectFrom_twice() =
87 testScope.runTest {
88 val presenter = SimplePresenter()
89
90 launch { presenter.collectFrom(infinitelySuspendingPagingData()) }.cancel()
91 launch { presenter.collectFrom(infinitelySuspendingPagingData()) }.cancel()
92 }
93
94 @Test
95 fun collectFrom_twiceConcurrently() =
96 testScope.runTest {
97 val presenter = SimplePresenter()
98
99 val job1 = launch { presenter.collectFrom(infinitelySuspendingPagingData()) }
100
101 // Ensure job1 is running.
102 assertTrue { job1.isActive }
103
104 val job2 = launch { presenter.collectFrom(infinitelySuspendingPagingData()) }
105
106 // job2 collection should complete job1 but not cancel.
107 assertFalse { job1.isCancelled }
108 assertTrue { job1.isCompleted }
109 job2.cancel()
110 }
111
112 @Test
113 fun retry() =
114 testScope.runTest {
115 val presenter = SimplePresenter()
116 val receiver = UiReceiverFake()
117
118 val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
119
120 presenter.retry()
121 assertEquals(1, receiver.retryEvents.size)
122
123 job.cancel()
124 }
125
126 @Test
127 fun refresh() =
128 testScope.runTest {
129 val presenter = SimplePresenter()
130 val receiver = UiReceiverFake()
131
132 val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
133
134 presenter.refresh()
135
136 assertEquals(1, receiver.refreshEvents.size)
137
138 job.cancel()
139 }
140
141 @Test
142 fun retrySentBeforeCollection() =
143 testScope.run {
144 val presenter = SimplePresenter()
145 val receiver = UiReceiverFake()
146
147 presenter.retry()
148
149 val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
150
151 assertEquals(1, receiver.retryEvents.size)
152
153 job.cancel()
154 }
155
156 @Test
157 fun refreshSentBeforeCollection() =
158 testScope.runTest {
159 val presenter = SimplePresenter()
160 val receiver = UiReceiverFake()
161
162 presenter.refresh()
163
164 val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
165
166 assertEquals(1, receiver.refreshEvents.size)
167
168 job.cancel()
169 }
170
171 @Test
172 fun uiReceiverSetImmediately() =
173 testScope.runTest {
174 val presenter = SimplePresenter()
175 val receiver = UiReceiverFake()
176 val pagingData1 = infinitelySuspendingPagingData(uiReceiver = receiver)
177
178 val job1 = launch { presenter.collectFrom(pagingData1) }
179 assertTrue(job1.isActive) // ensure job started
180
181 assertThat(receiver.refreshEvents).hasSize(0)
182
183 presenter.refresh()
184 // double check that the pagingdata's receiver was registered and had received refresh
185 // call
186 // before any PageEvent is collected/presented
187 assertThat(receiver.refreshEvents).hasSize(1)
188
189 job1.cancel()
190 }
191
192 @Test
193 fun hintReceiverSetAfterNewListPresented() =
194 testScope.runTest {
195 val presenter = SimplePresenter()
196
197 // first generation, load something so next gen can access index to trigger hint
198 val hintReceiver1 = HintReceiverFake()
199 val flow =
200 flowOf(
201 localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
202 )
203
204 val job1 = launch {
205 presenter.collectFrom(PagingData(flow, dummyUiReceiver, hintReceiver1))
206 }
207
208 // access any loaded item to make sure hint is sent
209 presenter[3]
210 assertThat(hintReceiver1.hints)
211 .containsExactly(
212 ViewportHint.Access(
213 pageOffset = 0,
214 indexInPage = 3,
215 presentedItemsBefore = 3,
216 presentedItemsAfter = 1,
217 originalPageOffsetFirst = 0,
218 originalPageOffsetLast = 0,
219 )
220 )
221
222 // trigger second generation
223 presenter.refresh()
224
225 // second generation
226 val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
227 val hintReceiver2 = HintReceiverFake()
228 val job2 = launch {
229 presenter.collectFrom(
230 PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver2)
231 )
232 }
233
234 // we send the initial load state. this should NOT cause second gen hint receiver
235 // to register
236 pageEventCh.trySend(localLoadStateUpdate(refreshLocal = Loading))
237 assertThat(presenter.nonNullLoadStateFlow.first())
238 .isEqualTo(localLoadStatesOf(refreshLocal = Loading))
239
240 // ensure both hint receivers are idle before sending a hint
241 assertThat(hintReceiver1.hints).isEmpty()
242 assertThat(hintReceiver2.hints).isEmpty()
243
244 // try sending a hint, should be sent to first receiver
245 presenter[4]
246 assertThat(hintReceiver1.hints).hasSize(1)
247 assertThat(hintReceiver2.hints).isEmpty()
248
249 // now we send actual refresh load and make sure its presented
250 pageEventCh.trySend(
251 localRefresh(
252 pages = listOf(TransformablePage(listOf(20, 21, 22, 23, 24))),
253 placeholdersBefore = 20,
254 placeholdersAfter = 75
255 ),
256 )
257 assertThat(presenter.snapshot().items).containsExactlyElementsIn(20 until 25)
258
259 // access any loaded item to make sure hint is sent to proper receiver
260 presenter[3]
261 // second receiver was registered and received the initial viewport hint
262 assertThat(hintReceiver1.hints).isEmpty()
263 assertThat(hintReceiver2.hints)
264 .containsExactly(
265 ViewportHint.Access(
266 pageOffset = 0,
267 indexInPage = -17,
268 presentedItemsBefore = -17,
269 presentedItemsAfter = 21,
270 originalPageOffsetFirst = 0,
271 originalPageOffsetLast = 0,
272 )
273 )
274
275 job2.cancel()
276 job1.cancel()
277 }
278
279 @Test fun refreshOnLatestGenerationReceiver() = refreshOnLatestGenerationReceiver(false)
280
281 @Test
282 fun refreshOnLatestGenerationReceiver_collectWithCachedIn() =
283 refreshOnLatestGenerationReceiver(true)
284
285 private fun refreshOnLatestGenerationReceiver(collectWithCachedIn: Boolean) =
286 runTest(collectWithCachedIn) { presenter, _, uiReceivers, hintReceivers ->
287 // first gen
288 advanceUntilIdle()
289 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
290
291 // append a page so we can cache an anchorPosition of [8]
292 presenter[8]
293 advanceUntilIdle()
294
295 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
296
297 // trigger gen 2, the refresh signal should to sent to gen 1
298 presenter.refresh()
299 assertThat(uiReceivers[0].refreshEvents).hasSize(1)
300 assertThat(uiReceivers[1].refreshEvents).hasSize(0)
301
302 // trigger gen 3, refresh signal should be sent to gen 2
303 presenter.refresh()
304 assertThat(uiReceivers[0].refreshEvents).hasSize(1)
305 assertThat(uiReceivers[1].refreshEvents).hasSize(1)
306 advanceUntilIdle()
307
308 assertThat(presenter.snapshot()).containsExactlyElementsIn(8 until 17)
309
310 // access any item to make sure gen 3 receiver is recipient of the hint
311 presenter[0]
312 assertThat(hintReceivers[2].hints)
313 .containsExactly(
314 ViewportHint.Access(
315 pageOffset = 0,
316 indexInPage = 0,
317 presentedItemsBefore = 0,
318 presentedItemsAfter = 8,
319 originalPageOffsetFirst = 0,
320 originalPageOffsetLast = 0,
321 )
322 )
323 }
324
325 @Test fun retryOnLatestGenerationReceiver() = retryOnLatestGenerationReceiver(false)
326
327 @Test
328 fun retryOnLatestGenerationReceiver_collectWithCachedIn() =
329 retryOnLatestGenerationReceiver(true)
330
331 private fun retryOnLatestGenerationReceiver(collectWithCachedIn: Boolean) =
332 runTest(collectWithCachedIn) { presenter, pagingSources, uiReceivers, hintReceivers ->
333
334 // first gen
335 advanceUntilIdle()
336 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
337
338 // append a page so we can cache an anchorPosition of [8]
339 presenter[8]
340 advanceUntilIdle()
341
342 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
343
344 // trigger gen 2, the refresh signal should be sent to gen 1
345 presenter.refresh()
346 assertThat(uiReceivers[0].refreshEvents).hasSize(1)
347 assertThat(uiReceivers[1].refreshEvents).hasSize(0)
348
349 // to recreate a real use-case of retry based on load error
350 pagingSources[1].errorNextLoad = true
351 advanceUntilIdle()
352 // presenter should still have first gen presenter
353 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
354
355 // retry should be sent to gen 2 even though it wasn't presented
356 presenter.retry()
357 assertThat(uiReceivers[0].retryEvents).hasSize(0)
358 assertThat(uiReceivers[1].retryEvents).hasSize(1)
359 advanceUntilIdle()
360
361 // will retry with the correct cached hint
362 assertThat(presenter.snapshot()).containsExactlyElementsIn(8 until 17)
363
364 // access any item to ensure gen 2 receiver was recipient of the initial hint
365 presenter[0]
366 assertThat(hintReceivers[1].hints)
367 .containsExactly(
368 ViewportHint.Access(
369 pageOffset = 0,
370 indexInPage = 0,
371 presentedItemsBefore = 0,
372 presentedItemsAfter = 8,
373 originalPageOffsetFirst = 0,
374 originalPageOffsetLast = 0,
375 )
376 )
377 }
378
379 @Test
380 fun refreshAfterStaticList() =
381 testScope.runTest {
382 val presenter = SimplePresenter()
383
384 val pagingData1 = PagingData.from(listOf(1, 2, 3))
385 val job1 = launch { presenter.collectFrom(pagingData1) }
386 assertTrue(job1.isCompleted)
387 assertThat(presenter.snapshot()).containsAtLeastElementsIn(listOf(1, 2, 3))
388
389 val uiReceiver = UiReceiverFake()
390 val pagingData2 = infinitelySuspendingPagingData(uiReceiver = uiReceiver)
391 val job2 = launch { presenter.collectFrom(pagingData2) }
392 assertTrue(job2.isActive)
393
394 // even though the second paging data never presented, it should be receiver of the
395 // refresh
396 presenter.refresh()
397 assertThat(uiReceiver.refreshEvents).hasSize(1)
398
399 job2.cancel()
400 }
401
402 @Test
403 fun retryAfterStaticList() =
404 testScope.runTest {
405 val presenter = SimplePresenter()
406
407 val pagingData1 = PagingData.from(listOf(1, 2, 3))
408 val job1 = launch { presenter.collectFrom(pagingData1) }
409 assertTrue(job1.isCompleted)
410 assertThat(presenter.snapshot()).containsAtLeastElementsIn(listOf(1, 2, 3))
411
412 val uiReceiver = UiReceiverFake()
413 val pagingData2 = infinitelySuspendingPagingData(uiReceiver = uiReceiver)
414 val job2 = launch { presenter.collectFrom(pagingData2) }
415 assertTrue(job2.isActive)
416
417 // even though the second paging data never presented, it should be receiver of the
418 // retry
419 presenter.retry()
420 assertThat(uiReceiver.retryEvents).hasSize(1)
421
422 job2.cancel()
423 }
424
425 @Test
426 fun hintCalculationBasedOnCurrentGeneration() =
427 testScope.runTest {
428 val presenter = SimplePresenter()
429
430 // first generation
431 val hintReceiver1 = HintReceiverFake()
432 val uiReceiver1 = UiReceiverFake()
433 val flow =
434 flowOf(
435 localRefresh(
436 pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4))),
437 placeholdersBefore = 0,
438 placeholdersAfter = 95
439 )
440 )
441
442 val job1 = launch {
443 presenter.collectFrom(PagingData(flow, uiReceiver1, hintReceiver1))
444 }
445 // access any item make sure hint is sent
446 presenter[3]
447 assertThat(hintReceiver1.hints)
448 .containsExactly(
449 ViewportHint.Access(
450 pageOffset = 0,
451 indexInPage = 3,
452 presentedItemsBefore = 3,
453 presentedItemsAfter = 1,
454 originalPageOffsetFirst = 0,
455 originalPageOffsetLast = 0,
456 )
457 )
458
459 // jump to another position, triggers invalidation
460 presenter[20]
461 assertThat(hintReceiver1.hints)
462 .isEqualTo(
463 listOf(
464 ViewportHint.Access(
465 pageOffset = 0,
466 indexInPage = 20,
467 presentedItemsBefore = 20,
468 presentedItemsAfter = -16,
469 originalPageOffsetFirst = 0,
470 originalPageOffsetLast = 0,
471 ),
472 )
473 )
474
475 // jump invalidation happens
476 presenter.refresh()
477 assertThat(uiReceiver1.refreshEvents).hasSize(1)
478
479 // second generation
480 val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
481 val hintReceiver2 = HintReceiverFake()
482 val job2 = launch {
483 presenter.collectFrom(
484 PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver2)
485 )
486 }
487
488 // jump to another position while second gen is loading. It should be sent to first gen.
489 presenter[40]
490 assertThat(hintReceiver1.hints)
491 .isEqualTo(
492 listOf(
493 ViewportHint.Access(
494 pageOffset = 0,
495 indexInPage = 40,
496 presentedItemsBefore = 40,
497 presentedItemsAfter = -36,
498 originalPageOffsetFirst = 0,
499 originalPageOffsetLast = 0,
500 ),
501 )
502 )
503 assertThat(hintReceiver2.hints).isEmpty()
504
505 // gen 2 initial load
506 pageEventCh.trySend(
507 localRefresh(
508 pages = listOf(TransformablePage(listOf(20, 21, 22, 23, 24))),
509 placeholdersBefore = 20,
510 placeholdersAfter = 75
511 ),
512 )
513 // access any item make sure hint is sent
514 presenter[3]
515 assertThat(hintReceiver2.hints)
516 .containsExactly(
517 ViewportHint.Access(
518 pageOffset = 0,
519 indexInPage = -17,
520 presentedItemsBefore = -17,
521 presentedItemsAfter = 21,
522 originalPageOffsetFirst = 0,
523 originalPageOffsetLast = 0,
524 )
525 )
526
527 // jumping to index 50. Hint.indexInPage should be adjusted accordingly based on
528 // the placeholdersBefore of new presenter. It should be
529 // (index - placeholdersBefore) = 50 - 20 = 30
530 presenter[50]
531 assertThat(hintReceiver2.hints)
532 .isEqualTo(
533 listOf(
534 ViewportHint.Access(
535 pageOffset = 0,
536 indexInPage = 30,
537 presentedItemsBefore = 30,
538 presentedItemsAfter = -26,
539 originalPageOffsetFirst = 0,
540 originalPageOffsetLast = 0,
541 ),
542 )
543 )
544
545 job2.cancel()
546 job1.cancel()
547 }
548
549 @Test
550 fun fetch_loadHintResentWhenUnfulfilled() =
551 testScope.runTest {
552 val presenter = SimplePresenter()
553
554 val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
555 pageEventCh.trySend(
556 localRefresh(
557 pages = listOf(TransformablePage(0, listOf(0, 1))),
558 placeholdersBefore = 4,
559 placeholdersAfter = 4,
560 )
561 )
562 pageEventCh.trySend(
563 localPrepend(
564 pages = listOf(TransformablePage(-1, listOf(-1, -2))),
565 placeholdersBefore = 2,
566 )
567 )
568 pageEventCh.trySend(
569 localAppend(
570 pages = listOf(TransformablePage(1, listOf(2, 3))),
571 placeholdersAfter = 2,
572 )
573 )
574
575 val hintReceiver = HintReceiverFake()
576 val job = launch {
577 presenter.collectFrom(
578 // Filter the original list of 10 items to 5, removing even numbers.
579 PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver).filter {
580 it % 2 != 0
581 }
582 )
583 }
584
585 // Initial state:
586 // [null, null, [-1], [1], [3], null, null]
587 assertNull(presenter[0])
588 assertThat(hintReceiver.hints)
589 .isEqualTo(
590 listOf(
591 ViewportHint.Access(
592 pageOffset = -1,
593 indexInPage = -2,
594 presentedItemsBefore = -2,
595 presentedItemsAfter = 4,
596 originalPageOffsetFirst = -1,
597 originalPageOffsetLast = 1
598 ),
599 )
600 )
601
602 // Insert a new page, PagingDataPresenter should try to resend hint since index 0 still
603 // points
604 // to a placeholder:
605 // [null, null, [], [-1], [1], [3], null, null]
606 pageEventCh.trySend(
607 localPrepend(
608 pages = listOf(TransformablePage(-2, listOf())),
609 placeholdersBefore = 2,
610 )
611 )
612 assertThat(hintReceiver.hints)
613 .isEqualTo(
614 listOf(
615 ViewportHint.Access(
616 pageOffset = -2,
617 indexInPage = -2,
618 presentedItemsBefore = -2,
619 presentedItemsAfter = 4,
620 originalPageOffsetFirst = -2,
621 originalPageOffsetLast = 1
622 )
623 )
624 )
625
626 // Now index 0 has been loaded:
627 // [[-3], [], [-1], [1], [3], null, null]
628 pageEventCh.trySend(
629 localPrepend(
630 pages = listOf(TransformablePage(-3, listOf(-3, -4))),
631 placeholdersBefore = 0,
632 source = loadStates(prepend = NotLoading.Complete)
633 )
634 )
635 assertThat(hintReceiver.hints).isEmpty()
636
637 // This index points to a valid placeholder that ends up removed by filter().
638 assertNull(presenter[5])
639 assertThat(hintReceiver.hints)
640 .containsExactly(
641 ViewportHint.Access(
642 pageOffset = 1,
643 indexInPage = 2,
644 presentedItemsBefore = 5,
645 presentedItemsAfter = -2,
646 originalPageOffsetFirst = -3,
647 originalPageOffsetLast = 1
648 )
649 )
650
651 // Should only resend the hint for index 5, since index 0 has already been loaded:
652 // [[-3], [], [-1], [1], [3], [], null, null]
653 pageEventCh.trySend(
654 localAppend(
655 pages = listOf(TransformablePage(2, listOf())),
656 placeholdersAfter = 2,
657 source = loadStates(prepend = NotLoading.Complete)
658 )
659 )
660 assertThat(hintReceiver.hints)
661 .isEqualTo(
662 listOf(
663 ViewportHint.Access(
664 pageOffset = 2,
665 indexInPage = 1,
666 presentedItemsBefore = 5,
667 presentedItemsAfter = -2,
668 originalPageOffsetFirst = -3,
669 originalPageOffsetLast = 2
670 )
671 )
672 )
673
674 // Index 5 hasn't loaded, but we are at the end of the list:
675 // [[-3], [], [-1], [1], [3], [], [5]]
676 pageEventCh.trySend(
677 localAppend(
678 pages = listOf(TransformablePage(3, listOf(4, 5))),
679 placeholdersAfter = 0,
680 source = loadStates(prepend = NotLoading.Complete, append = NotLoading.Complete)
681 )
682 )
683 assertThat(hintReceiver.hints).isEmpty()
684
685 job.cancel()
686 }
687
688 @Test
689 fun fetch_loadHintResentUnlessPageDropped() =
690 testScope.runTest {
691 val presenter = SimplePresenter()
692
693 val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
694 pageEventCh.trySend(
695 localRefresh(
696 pages = listOf(TransformablePage(0, listOf(0, 1))),
697 placeholdersBefore = 4,
698 placeholdersAfter = 4,
699 )
700 )
701 pageEventCh.trySend(
702 localPrepend(
703 pages = listOf(TransformablePage(-1, listOf(-1, -2))),
704 placeholdersBefore = 2,
705 )
706 )
707 pageEventCh.trySend(
708 localAppend(
709 pages = listOf(TransformablePage(1, listOf(2, 3))),
710 placeholdersAfter = 2,
711 )
712 )
713
714 val hintReceiver = HintReceiverFake()
715 val job = launch {
716 presenter.collectFrom(
717 // Filter the original list of 10 items to 5, removing even numbers.
718 PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver).filter {
719 it % 2 != 0
720 }
721 )
722 }
723
724 // Initial state:
725 // [null, null, [-1], [1], [3], null, null]
726 assertNull(presenter[0])
727 assertThat(hintReceiver.hints)
728 .containsExactly(
729 ViewportHint.Access(
730 pageOffset = -1,
731 indexInPage = -2,
732 presentedItemsBefore = -2,
733 presentedItemsAfter = 4,
734 originalPageOffsetFirst = -1,
735 originalPageOffsetLast = 1
736 )
737 )
738
739 // Insert a new page, PagingDataPresenter should try to resend hint since index 0 still
740 // points
741 // to a placeholder:
742 // [null, null, [], [-1], [1], [3], null, null]
743 pageEventCh.trySend(
744 localPrepend(
745 pages = listOf(TransformablePage(-2, listOf())),
746 placeholdersBefore = 2,
747 )
748 )
749 assertThat(hintReceiver.hints)
750 .isEqualTo(
751 listOf(
752 ViewportHint.Access(
753 pageOffset = -2,
754 indexInPage = -2,
755 presentedItemsBefore = -2,
756 presentedItemsAfter = 4,
757 originalPageOffsetFirst = -2,
758 originalPageOffsetLast = 1
759 )
760 )
761 )
762
763 // Drop the previous page, which reset resendable index state in the PREPEND direction.
764 // [null, null, [-1], [1], [3], null, null]
765 pageEventCh.trySend(
766 Drop(
767 loadType = PREPEND,
768 minPageOffset = -2,
769 maxPageOffset = -2,
770 placeholdersRemaining = 2
771 )
772 )
773
774 // Re-insert the previous page, which should not trigger resending the index due to
775 // previous page drop:
776 // [[-3], [], [-1], [1], [3], null, null]
777 pageEventCh.trySend(
778 localPrepend(
779 pages = listOf(TransformablePage(-2, listOf())),
780 placeholdersBefore = 2,
781 )
782 )
783
784 job.cancel()
785 }
786
787 @Test
788 fun peek() =
789 testScope.runTest {
790 val presenter = SimplePresenter()
791 val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
792 pageEventCh.trySend(
793 localRefresh(
794 pages = listOf(TransformablePage(0, listOf(0, 1))),
795 placeholdersBefore = 4,
796 placeholdersAfter = 4,
797 )
798 )
799 pageEventCh.trySend(
800 localPrepend(
801 pages = listOf(TransformablePage(-1, listOf(-1, -2))),
802 placeholdersBefore = 2,
803 )
804 )
805 pageEventCh.trySend(
806 localAppend(
807 pages = listOf(TransformablePage(1, listOf(2, 3))),
808 placeholdersAfter = 2,
809 )
810 )
811
812 val hintReceiver = HintReceiverFake()
813 val job = launch {
814 presenter.collectFrom(
815 // Filter the original list of 10 items to 5, removing even numbers.
816 PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver)
817 )
818 }
819
820 // Check that peek fetches the correct placeholder
821 assertThat(presenter.peek(4)).isEqualTo(0)
822
823 // Check that peek fetches the correct placeholder
824 assertNull(presenter.peek(0))
825
826 // Check that peek does not trigger page fetch.
827 assertThat(hintReceiver.hints).isEmpty()
828
829 job.cancel()
830 }
831
832 @Test
833 fun onPagingDataPresentedListener_empty() =
834 testScope.runTest {
835 val presenter = SimplePresenter()
836 val listenerEvents = mutableListOf<Unit>()
837 presenter.addOnPagesUpdatedListener { listenerEvents.add(Unit) }
838
839 presenter.collectFrom(PagingData.empty())
840 assertThat(listenerEvents.size).isEqualTo(1)
841
842 // No change to LoadState or presented list should still trigger the listener.
843 presenter.collectFrom(PagingData.empty())
844 assertThat(listenerEvents.size).isEqualTo(2)
845
846 val pager = Pager(PagingConfig(pageSize = 1)) { TestPagingSource(items = listOf()) }
847 val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
848
849 // Should wait for new generation to load and apply it first.
850 assertThat(listenerEvents.size).isEqualTo(2)
851
852 advanceUntilIdle()
853 assertThat(listenerEvents.size).isEqualTo(3)
854
855 job.cancel()
856 }
857
858 @Test
859 fun onPagingDataPresentedListener_insertDrop() =
860 testScope.runTest {
861 val presenter = SimplePresenter()
862 val listenerEvents = mutableListOf<Unit>()
863 presenter.addOnPagesUpdatedListener { listenerEvents.add(Unit) }
864
865 val pager =
866 Pager(PagingConfig(pageSize = 1, maxSize = 4), initialKey = 50) {
867 TestPagingSource()
868 }
869 val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
870
871 // Should wait for new generation to load and apply it first.
872 assertThat(listenerEvents.size).isEqualTo(0)
873
874 advanceUntilIdle()
875 assertThat(listenerEvents.size).isEqualTo(1)
876
877 // Trigger PREPEND.
878 presenter[50]
879 assertThat(listenerEvents.size).isEqualTo(1)
880 advanceUntilIdle()
881 assertThat(listenerEvents.size).isEqualTo(2)
882
883 // Trigger APPEND + Drop
884 presenter[52]
885 assertThat(listenerEvents.size).isEqualTo(2)
886 advanceUntilIdle()
887 assertThat(listenerEvents.size).isEqualTo(4)
888
889 job.cancel()
890 }
891
892 @Test
893 fun onPagingDataPresentedFlow_empty() =
894 testScope.runTest {
895 val presenter = SimplePresenter()
896 val listenerEvents = mutableListOf<Unit>()
897 val job1 = launch { presenter.onPagesUpdatedFlow.collect { listenerEvents.add(Unit) } }
898
899 presenter.collectFrom(PagingData.empty())
900 assertThat(listenerEvents.size).isEqualTo(1)
901
902 // No change to LoadState or presented list should still trigger the listener.
903 presenter.collectFrom(PagingData.empty())
904 assertThat(listenerEvents.size).isEqualTo(2)
905
906 val pager = Pager(PagingConfig(pageSize = 1)) { TestPagingSource(items = listOf()) }
907 val job2 = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
908
909 // Should wait for new generation to load and apply it first.
910 assertThat(listenerEvents.size).isEqualTo(2)
911
912 advanceUntilIdle()
913 assertThat(listenerEvents.size).isEqualTo(3)
914
915 job1.cancel()
916 job2.cancel()
917 }
918
919 @Test
920 fun onPagingDataPresentedFlow_insertDrop() =
921 testScope.runTest {
922 val presenter = SimplePresenter()
923 val listenerEvents = mutableListOf<Unit>()
924 val job1 = launch { presenter.onPagesUpdatedFlow.collect { listenerEvents.add(Unit) } }
925
926 val pager =
927 Pager(PagingConfig(pageSize = 1, maxSize = 4), initialKey = 50) {
928 TestPagingSource()
929 }
930 val job2 = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
931
932 // Should wait for new generation to load and apply it first.
933 assertThat(listenerEvents.size).isEqualTo(0)
934
935 advanceUntilIdle()
936 assertThat(listenerEvents.size).isEqualTo(1)
937
938 // Trigger PREPEND.
939 presenter[50]
940 assertThat(listenerEvents.size).isEqualTo(1)
941 advanceUntilIdle()
942 assertThat(listenerEvents.size).isEqualTo(2)
943
944 // Trigger APPEND + Drop
945 presenter[52]
946 assertThat(listenerEvents.size).isEqualTo(2)
947 advanceUntilIdle()
948 assertThat(listenerEvents.size).isEqualTo(4)
949
950 job1.cancel()
951 job2.cancel()
952 }
953
954 @Test
955 fun onPagingDataPresentedFlow_buffer() =
956 testScope.runTest {
957 val presenter = SimplePresenter()
958 val listenerEvents = mutableListOf<Unit>()
959
960 // Trigger update, which should get ignored due to onPagesUpdatedFlow being hot.
961 presenter.collectFrom(PagingData.empty())
962
963 val job = launch {
964 presenter.onPagesUpdatedFlow.collect {
965 listenerEvents.add(Unit)
966 // Await advanceUntilIdle() before accepting another event.
967 delay(100)
968 }
969 }
970
971 // Previous update before collection happened should be ignored.
972 assertThat(listenerEvents.size).isEqualTo(0)
973
974 // Trigger update; should get immediately received.
975 presenter.collectFrom(PagingData.empty())
976 assertThat(listenerEvents.size).isEqualTo(1)
977
978 // Trigger 64 update while collector is still processing; should all get buffered.
979 repeat(64) { presenter.collectFrom(PagingData.empty()) }
980
981 // Trigger another update while collector is still processing; should cause event to
982 // drop.
983 presenter.collectFrom(PagingData.empty())
984
985 // Await all; we should now receive the buffered event.
986 advanceUntilIdle()
987 assertThat(listenerEvents.size).isEqualTo(65)
988
989 job.cancel()
990 }
991
992 @Test
993 fun loadStateFlow_synchronouslyUpdates() =
994 testScope.runTest {
995 val presenter = SimplePresenter()
996 var combinedLoadStates: CombinedLoadStates? = null
997 var itemCount = -1
998 val loadStateJob = launch {
999 presenter.nonNullLoadStateFlow.collect {
1000 combinedLoadStates = it
1001 itemCount = presenter.size
1002 }
1003 }
1004
1005 val pager =
1006 Pager(
1007 config =
1008 PagingConfig(
1009 pageSize = 10,
1010 enablePlaceholders = false,
1011 initialLoadSize = 10,
1012 prefetchDistance = 1
1013 ),
1014 initialKey = 50
1015 ) {
1016 TestPagingSource()
1017 }
1018 val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
1019
1020 // Initial refresh
1021 advanceUntilIdle()
1022 assertEquals(localLoadStatesOf(), combinedLoadStates)
1023 assertEquals(10, itemCount)
1024 assertEquals(10, presenter.size)
1025
1026 // Append
1027 presenter[9]
1028 advanceUntilIdle()
1029 assertEquals(localLoadStatesOf(), combinedLoadStates)
1030 assertEquals(20, itemCount)
1031 assertEquals(20, presenter.size)
1032
1033 // Prepend
1034 presenter[0]
1035 advanceUntilIdle()
1036 assertEquals(localLoadStatesOf(), combinedLoadStates)
1037 assertEquals(30, itemCount)
1038 assertEquals(30, presenter.size)
1039
1040 job.cancel()
1041 loadStateJob.cancel()
1042 }
1043
1044 @Test
1045 fun loadStateFlow_hasNoInitialValue() =
1046 testScope.runTest {
1047 val presenter = SimplePresenter()
1048
1049 // Should not immediately emit without a real value to a new collector.
1050 val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1051 val loadStateJob = launch {
1052 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1053 }
1054 assertThat(combinedLoadStates).isEmpty()
1055
1056 // Add a real value and now we should emit to collector.
1057 presenter.collectFrom(
1058 PagingData.empty(
1059 sourceLoadStates =
1060 loadStates(prepend = NotLoading.Complete, append = NotLoading.Complete)
1061 )
1062 )
1063 assertThat(combinedLoadStates)
1064 .containsExactly(
1065 localLoadStatesOf(
1066 prependLocal = NotLoading.Complete,
1067 appendLocal = NotLoading.Complete,
1068 )
1069 )
1070
1071 // Should emit real values to new collectors immediately
1072 val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1073 val newLoadStateJob = launch {
1074 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1075 }
1076 assertThat(newCombinedLoadStates)
1077 .containsExactly(
1078 localLoadStatesOf(
1079 prependLocal = NotLoading.Complete,
1080 appendLocal = NotLoading.Complete,
1081 )
1082 )
1083
1084 loadStateJob.cancel()
1085 newLoadStateJob.cancel()
1086 }
1087
1088 @Test
1089 fun loadStateFlow_preservesLoadStatesOnEmptyList() =
1090 testScope.runTest {
1091 val presenter = SimplePresenter()
1092
1093 // Should not immediately emit without a real value to a new collector.
1094 val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1095 val loadStateJob = launch {
1096 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1097 }
1098 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1099
1100 // Send a static list without load states, which should not send anything.
1101 presenter.collectFrom(PagingData.empty())
1102 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1103
1104 // Send a real LoadStateUpdate.
1105 presenter.collectFrom(
1106 PagingData(
1107 flow =
1108 flowOf(
1109 remoteLoadStateUpdate(
1110 refreshLocal = Loading,
1111 prependLocal = Loading,
1112 appendLocal = Loading,
1113 refreshRemote = Loading,
1114 prependRemote = Loading,
1115 appendRemote = Loading,
1116 )
1117 ),
1118 uiReceiver = PagingData.NOOP_UI_RECEIVER,
1119 hintReceiver = PagingData.NOOP_HINT_RECEIVER
1120 )
1121 )
1122 assertThat(combinedLoadStates.getAllAndClear())
1123 .containsExactly(
1124 remoteLoadStatesOf(
1125 refresh = Loading,
1126 prepend = Loading,
1127 append = Loading,
1128 refreshLocal = Loading,
1129 prependLocal = Loading,
1130 appendLocal = Loading,
1131 refreshRemote = Loading,
1132 prependRemote = Loading,
1133 appendRemote = Loading,
1134 )
1135 )
1136
1137 // Send a static list without load states, which should preserve the previous state.
1138 presenter.collectFrom(PagingData.empty())
1139 // Existing observers should not receive any updates
1140 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1141 // New observers should receive the previous state.
1142 val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1143 val newLoadStateJob = launch {
1144 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1145 }
1146 assertThat(newCombinedLoadStates.getAllAndClear())
1147 .containsExactly(
1148 remoteLoadStatesOf(
1149 refresh = Loading,
1150 prepend = Loading,
1151 append = Loading,
1152 refreshLocal = Loading,
1153 prependLocal = Loading,
1154 appendLocal = Loading,
1155 refreshRemote = Loading,
1156 prependRemote = Loading,
1157 appendRemote = Loading,
1158 )
1159 )
1160
1161 loadStateJob.cancel()
1162 newLoadStateJob.cancel()
1163 }
1164
1165 @Test
1166 fun loadStateFlow_preservesLoadStatesOnStaticList() =
1167 testScope.runTest {
1168 val presenter = SimplePresenter()
1169
1170 // Should not immediately emit without a real value to a new collector.
1171 val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1172 val loadStateJob = launch {
1173 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1174 }
1175 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1176
1177 // Send a static list without load states, which should not send anything.
1178 presenter.collectFrom(PagingData.from(listOf(1)))
1179 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1180
1181 // Send a real LoadStateUpdate.
1182 presenter.collectFrom(
1183 PagingData(
1184 flow =
1185 flowOf(
1186 remoteLoadStateUpdate(
1187 refreshLocal = Loading,
1188 prependLocal = Loading,
1189 appendLocal = Loading,
1190 refreshRemote = Loading,
1191 prependRemote = Loading,
1192 appendRemote = Loading,
1193 )
1194 ),
1195 uiReceiver = PagingData.NOOP_UI_RECEIVER,
1196 hintReceiver = PagingData.NOOP_HINT_RECEIVER
1197 )
1198 )
1199 assertThat(combinedLoadStates.getAllAndClear())
1200 .containsExactly(
1201 remoteLoadStatesOf(
1202 refresh = Loading,
1203 prepend = Loading,
1204 append = Loading,
1205 refreshLocal = Loading,
1206 prependLocal = Loading,
1207 appendLocal = Loading,
1208 refreshRemote = Loading,
1209 prependRemote = Loading,
1210 appendRemote = Loading,
1211 )
1212 )
1213
1214 // Send a static list without load states, which should preserve the previous state.
1215 presenter.collectFrom(PagingData.from(listOf(1)))
1216 // Existing observers should not receive any updates
1217 assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1218 // New observers should receive the previous state.
1219 val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1220 val newLoadStateJob = launch {
1221 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1222 }
1223 assertThat(newCombinedLoadStates.getAllAndClear())
1224 .containsExactly(
1225 remoteLoadStatesOf(
1226 refresh = Loading,
1227 prepend = Loading,
1228 append = Loading,
1229 refreshLocal = Loading,
1230 prependLocal = Loading,
1231 appendLocal = Loading,
1232 refreshRemote = Loading,
1233 prependRemote = Loading,
1234 appendRemote = Loading,
1235 )
1236 )
1237
1238 loadStateJob.cancel()
1239 newLoadStateJob.cancel()
1240 }
1241
1242 @Test
1243 fun loadStateFlow_deduplicate() =
1244 testScope.runTest {
1245 val presenter = SimplePresenter()
1246
1247 val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1248 backgroundScope.launch {
1249 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1250 }
1251
1252 presenter.collectFrom(
1253 PagingData(
1254 flow =
1255 flowOf(
1256 remoteLoadStateUpdate(
1257 prependLocal = Loading,
1258 appendLocal = Loading,
1259 ),
1260 remoteLoadStateUpdate(
1261 appendLocal = Loading,
1262 ),
1263 // duplicate update
1264 remoteLoadStateUpdate(
1265 appendLocal = Loading,
1266 ),
1267 ),
1268 uiReceiver = PagingData.NOOP_UI_RECEIVER,
1269 hintReceiver = PagingData.NOOP_HINT_RECEIVER
1270 )
1271 )
1272 advanceUntilIdle()
1273 assertThat(combinedLoadStates)
1274 .containsExactly(
1275 remoteLoadStatesOf(
1276 prependLocal = Loading,
1277 appendLocal = Loading,
1278 ),
1279 remoteLoadStatesOf(
1280 appendLocal = Loading,
1281 )
1282 )
1283 }
1284
1285 @Test
1286 fun loadStateFlowListeners_deduplicate() =
1287 testScope.runTest {
1288 val presenter = SimplePresenter()
1289 val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1290
1291 presenter.addLoadStateListener { combinedLoadStates.add(it) }
1292
1293 presenter.collectFrom(
1294 PagingData(
1295 flow =
1296 flowOf(
1297 remoteLoadStateUpdate(
1298 prependLocal = Loading,
1299 appendLocal = Loading,
1300 ),
1301 remoteLoadStateUpdate(
1302 appendLocal = Loading,
1303 ),
1304 // duplicate update
1305 remoteLoadStateUpdate(
1306 appendLocal = Loading,
1307 ),
1308 ),
1309 uiReceiver = PagingData.NOOP_UI_RECEIVER,
1310 hintReceiver = PagingData.NOOP_HINT_RECEIVER
1311 )
1312 )
1313 advanceUntilIdle()
1314 assertThat(combinedLoadStates)
1315 .containsExactly(
1316 remoteLoadStatesOf(
1317 prependLocal = Loading,
1318 appendLocal = Loading,
1319 ),
1320 remoteLoadStatesOf(
1321 appendLocal = Loading,
1322 )
1323 )
1324 }
1325
1326 @Test
1327 fun addLoadStateListener_SynchronouslyUpdates() =
1328 testScope.runTest {
1329 val presenter = SimplePresenter()
1330 var combinedLoadStates: CombinedLoadStates? = null
1331 var itemCount = -1
1332 presenter.addLoadStateListener {
1333 combinedLoadStates = it
1334 itemCount = presenter.size
1335 }
1336
1337 val pager =
1338 Pager(
1339 config =
1340 PagingConfig(
1341 pageSize = 10,
1342 enablePlaceholders = false,
1343 initialLoadSize = 10,
1344 prefetchDistance = 1
1345 ),
1346 initialKey = 50
1347 ) {
1348 TestPagingSource()
1349 }
1350 val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
1351
1352 // Initial refresh
1353 advanceUntilIdle()
1354 assertEquals(localLoadStatesOf(), combinedLoadStates)
1355 assertEquals(10, itemCount)
1356 assertEquals(10, presenter.size)
1357
1358 // Append
1359 presenter[9]
1360 advanceUntilIdle()
1361 assertEquals(localLoadStatesOf(), combinedLoadStates)
1362 assertEquals(20, itemCount)
1363 assertEquals(20, presenter.size)
1364
1365 // Prepend
1366 presenter[0]
1367 advanceUntilIdle()
1368 assertEquals(localLoadStatesOf(), combinedLoadStates)
1369 assertEquals(30, itemCount)
1370 assertEquals(30, presenter.size)
1371
1372 job.cancel()
1373 }
1374
1375 @Test
1376 fun addLoadStateListener_hasNoInitialValue() =
1377 testScope.runTest {
1378 val presenter = SimplePresenter()
1379 val combinedLoadStateCapture = CombinedLoadStatesCapture()
1380
1381 // Adding a new listener without a real value should not trigger it.
1382 presenter.addLoadStateListener(combinedLoadStateCapture)
1383 assertThat(combinedLoadStateCapture.newEvents()).isEmpty()
1384
1385 // Add a real value and now the listener should trigger.
1386 presenter.collectFrom(
1387 PagingData.empty(
1388 sourceLoadStates =
1389 loadStates(
1390 prepend = NotLoading.Complete,
1391 append = NotLoading.Complete,
1392 )
1393 )
1394 )
1395 assertThat(combinedLoadStateCapture.newEvents())
1396 .containsExactly(
1397 localLoadStatesOf(
1398 prependLocal = NotLoading.Complete,
1399 appendLocal = NotLoading.Complete,
1400 )
1401 )
1402
1403 // Should emit real values to new listeners immediately
1404 val newCombinedLoadStateCapture = CombinedLoadStatesCapture()
1405 presenter.addLoadStateListener(newCombinedLoadStateCapture)
1406 assertThat(newCombinedLoadStateCapture.newEvents())
1407 .containsExactly(
1408 localLoadStatesOf(
1409 prependLocal = NotLoading.Complete,
1410 appendLocal = NotLoading.Complete,
1411 )
1412 )
1413 }
1414
1415 @Test
1416 fun uncaughtException() =
1417 testScope.runTest {
1418 val presenter = SimplePresenter()
1419 val pager =
1420 Pager(
1421 PagingConfig(1),
1422 ) {
1423 object : PagingSource<Int, Int>() {
1424 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
1425 throw IllegalStateException()
1426 }
1427
1428 override fun getRefreshKey(state: PagingState<Int, Int>): Int? = null
1429 }
1430 }
1431
1432 val pagingData = pager.flow.first()
1433 val deferred = async(Job()) { presenter.collectFrom(pagingData) }
1434
1435 advanceUntilIdle()
1436 assertFailsWith<IllegalStateException> { deferred.await() }
1437 }
1438
1439 @Test
1440 fun handledLoadResultInvalid() =
1441 testScope.runTest {
1442 val presenter = SimplePresenter()
1443 var generation = 0
1444 val pager =
1445 Pager(
1446 PagingConfig(1),
1447 ) {
1448 TestPagingSource().also {
1449 if (generation == 0) {
1450 it.nextLoadResult = LoadResult.Invalid()
1451 }
1452 generation++
1453 }
1454 }
1455
1456 val pagingData = pager.flow.first()
1457 val deferred = async {
1458 // only returns if flow is closed, or work canclled, or exception thrown
1459 // in this case it should cancel due LoadResult.Invalid causing collectFrom to
1460 // return
1461 presenter.collectFrom(pagingData)
1462 }
1463
1464 advanceUntilIdle()
1465 // this will return only if presenter.collectFrom returns
1466 deferred.await()
1467 }
1468
1469 @Test fun refresh_pagingDataEvent() = refresh_pagingDataEvent(false)
1470
1471 @Test fun refresh_pagingDataEvent_collectWithCachedIn() = refresh_pagingDataEvent(true)
1472
1473 private fun refresh_pagingDataEvent(collectWithCachedIn: Boolean) =
1474 runTest(collectWithCachedIn, initialKey = 50) { presenter, _, _, _ ->
1475 // execute queued initial REFRESH
1476 advanceUntilIdle()
1477
1478 val event =
1479 PageStore(
1480 pages =
1481 listOf(
1482 TransformablePage(
1483 originalPageOffsets = intArrayOf(0),
1484 data = listOf(50, 51, 52, 53, 54, 55, 56, 57, 58),
1485 hintOriginalPageOffset = 0,
1486 hintOriginalIndices = null,
1487 )
1488 ),
1489 placeholdersBefore = 0,
1490 placeholdersAfter = 0,
1491 )
1492 as PlaceholderPaddedList<Int>
1493
1494 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1495 assertThat(presenter.newEvents())
1496 .containsExactly(
1497 PagingDataEvent.Refresh(
1498 previousList = PageStore.initial<Int>(null) as PlaceholderPaddedList<Int>,
1499 newList = event
1500 )
1501 )
1502
1503 presenter.refresh()
1504
1505 // execute second REFRESH load
1506 advanceUntilIdle()
1507
1508 // // second refresh loads from initialKey = 0 because anchorPosition/refreshKey is null
1509 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1510 assertThat(presenter.newEvents())
1511 .containsExactly(
1512 PagingDataEvent.Refresh(
1513 previousList = event,
1514 newList =
1515 PageStore(
1516 pages =
1517 listOf(
1518 TransformablePage(
1519 originalPageOffsets = intArrayOf(0),
1520 data = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8),
1521 hintOriginalPageOffset = 0,
1522 hintOriginalIndices = null,
1523 )
1524 ),
1525 placeholdersBefore = 0,
1526 placeholdersAfter = 0,
1527 )
1528 as PlaceholderPaddedList<Int>
1529 )
1530 )
1531 }
1532
1533 @Test fun append_pagingDataEvent() = append_pagingDataEvent(false)
1534
1535 @Test fun append_pagingDataEvent_collectWithCachedIn() = append_pagingDataEvent(true)
1536
1537 private fun append_pagingDataEvent(collectWithCachedIn: Boolean) =
1538 runTest(collectWithCachedIn) { presenter, _, _, _ ->
1539
1540 // initial REFRESH
1541 advanceUntilIdle()
1542
1543 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1544
1545 // trigger append
1546 presenter[7]
1547 advanceUntilIdle()
1548
1549 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
1550 assertThat(presenter.newEvents().last())
1551 .isEqualTo(
1552 PagingDataEvent.Append(
1553 startIndex = 9,
1554 inserted = listOf(9, 10, 11),
1555 newPlaceholdersAfter = 0,
1556 oldPlaceholdersAfter = 0
1557 )
1558 )
1559 }
1560
1561 @Test fun appendDrop_pagingDataEvent() = appendDrop_pagingDataEvent(false)
1562
1563 @Test fun appendDrop_pagingDataEvent_collectWithCachedIn() = appendDrop_pagingDataEvent(true)
1564
1565 private fun appendDrop_pagingDataEvent(collectWithCachedIn: Boolean) =
1566 runTest(
1567 collectWithCachedIn,
1568 initialKey = 96,
1569 config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1570 ) { presenter, _, _, _ ->
1571 // initial REFRESH
1572 advanceUntilIdle()
1573
1574 assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 99)
1575
1576 // trigger append to reach max page size
1577 presenter[2]
1578 advanceUntilIdle()
1579
1580 assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 100)
1581 assertThat(presenter.newEvents().last())
1582 .isEqualTo(
1583 PagingDataEvent.Append(
1584 startIndex = 3,
1585 inserted = listOf(99),
1586 newPlaceholdersAfter = 0,
1587 oldPlaceholdersAfter = 0
1588 )
1589 )
1590 // trigger prepend and drop from append direction
1591 presenter[0]
1592 advanceUntilIdle()
1593
1594 assertThat(presenter.snapshot()).containsExactlyElementsIn(95 until 99)
1595 // drop is processed before inserts
1596 assertThat(presenter.newEvents().first())
1597 .isEqualTo(
1598 PagingDataEvent.DropAppend<Int>(
1599 startIndex = 3,
1600 dropCount = 1,
1601 newPlaceholdersAfter = 0,
1602 oldPlaceholdersAfter = 0
1603 )
1604 )
1605 }
1606
1607 @Test fun prepend_pagingDataEvent() = prepend_pagingDataEvent(false)
1608
1609 @Test fun prepend_pagingDataEvent_collectWithCachedIn() = prepend_pagingDataEvent(true)
1610
1611 private fun prepend_pagingDataEvent(collectWithCachedIn: Boolean) =
1612 runTest(collectWithCachedIn, initialKey = 50) { presenter, _, _, _ ->
1613
1614 // initial REFRESH
1615 advanceUntilIdle()
1616
1617 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1618
1619 // trigger prepend
1620 presenter[0]
1621 advanceUntilIdle()
1622
1623 assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
1624 assertThat(presenter.newEvents().last())
1625 .isEqualTo(
1626 PagingDataEvent.Prepend(
1627 inserted = listOf(47, 48, 49),
1628 newPlaceholdersBefore = 0,
1629 oldPlaceholdersBefore = 0
1630 )
1631 )
1632 }
1633
1634 @Test fun prependDrop_pagingDataEvent() = prependDrop_pagingDataEvent(false)
1635
1636 @Test fun prependDrop_pagingDataEvent_collectWithCachedIn() = prependDrop_pagingDataEvent(true)
1637
1638 private fun prependDrop_pagingDataEvent(collectWithCachedIn: Boolean) =
1639 runTest(
1640 collectWithCachedIn,
1641 initialKey = 1,
1642 config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1643 ) { presenter, _, _, _ ->
1644 // initial REFRESH
1645 advanceUntilIdle()
1646
1647 assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 4)
1648
1649 // trigger prepend to reach max page size
1650 presenter[0]
1651 advanceUntilIdle()
1652
1653 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 4)
1654 assertThat(presenter.newEvents().last())
1655 .isEqualTo(
1656 PagingDataEvent.Prepend(
1657 inserted = listOf(0),
1658 newPlaceholdersBefore = 0,
1659 oldPlaceholdersBefore = 0
1660 )
1661 )
1662
1663 // trigger append and drop from prepend direction
1664 presenter[3]
1665 advanceUntilIdle()
1666
1667 assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 5)
1668 // drop is processed before insert
1669 assertThat(presenter.newEvents().first())
1670 .isEqualTo(
1671 PagingDataEvent.DropPrepend<Int>(
1672 dropCount = 1,
1673 newPlaceholdersBefore = 0,
1674 oldPlaceholdersBefore = 0
1675 )
1676 )
1677 }
1678
1679 @Test fun refresh_loadStates() = refresh_loadStates(false)
1680
1681 @Test fun refresh_loadStates_collectWithCachedIn() = refresh_loadStates(true)
1682
1683 private fun refresh_loadStates(collectWithCachedIn: Boolean) =
1684 runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
1685 val collectLoadStates = launch { presenter.collectLoadStates() }
1686
1687 // execute queued initial REFRESH
1688 advanceUntilIdle()
1689
1690 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1691 assertThat(presenter.newCombinedLoadStates())
1692 .containsExactly(
1693 localLoadStatesOf(refreshLocal = Loading),
1694 localLoadStatesOf(),
1695 )
1696
1697 presenter.refresh()
1698
1699 // execute second REFRESH load
1700 advanceUntilIdle()
1701
1702 // second refresh loads from initialKey = 0 because anchorPosition/refreshKey is null
1703 assertThat(pagingSources.size).isEqualTo(2)
1704 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1705 assertThat(presenter.newCombinedLoadStates())
1706 .containsExactly(
1707 localLoadStatesOf(refreshLocal = Loading),
1708 localLoadStatesOf(prependLocal = NotLoading.Complete)
1709 )
1710
1711 collectLoadStates.cancel()
1712 }
1713
1714 @Test
1715 fun refresh_loadStates_afterEndOfPagination() = refresh_loadStates_afterEndOfPagination(false)
1716
1717 @Test
1718 fun refresh_loadStates_afterEndOfPagination_collectWithCachedIn() =
1719 refresh_loadStates_afterEndOfPagination(true)
1720
1721 private fun refresh_loadStates_afterEndOfPagination(collectWithCachedIn: Boolean) =
1722 runTest(collectWithCachedIn) { presenter, _, _, _ ->
1723 val loadStateCallbacks = mutableListOf<CombinedLoadStates>()
1724 presenter.addLoadStateListener { loadStateCallbacks.add(it) }
1725 val collectLoadStates = launch { presenter.collectLoadStates() }
1726 // execute initial refresh
1727 advanceUntilIdle()
1728 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1729 assertThat(presenter.newCombinedLoadStates())
1730 .containsExactly(
1731 localLoadStatesOf(refreshLocal = Loading),
1732 localLoadStatesOf(
1733 refreshLocal = NotLoading(endOfPaginationReached = false),
1734 prependLocal = NotLoading(endOfPaginationReached = true)
1735 )
1736 )
1737 loadStateCallbacks.clear()
1738 presenter.refresh()
1739 // after a refresh, make sure the loading event comes in 1 piece w/ the end of
1740 // pagination
1741 // reset
1742 advanceUntilIdle()
1743 assertThat(presenter.newCombinedLoadStates())
1744 .containsExactly(
1745 localLoadStatesOf(
1746 refreshLocal = Loading,
1747 prependLocal = NotLoading(endOfPaginationReached = false)
1748 ),
1749 localLoadStatesOf(
1750 refreshLocal = NotLoading(endOfPaginationReached = false),
1751 prependLocal = NotLoading(endOfPaginationReached = true)
1752 ),
1753 )
1754 assertThat(loadStateCallbacks)
1755 .containsExactly(
1756 localLoadStatesOf(
1757 refreshLocal = Loading,
1758 prependLocal = NotLoading(endOfPaginationReached = false)
1759 ),
1760 localLoadStatesOf(
1761 refreshLocal = NotLoading(endOfPaginationReached = false),
1762 prependLocal = NotLoading(endOfPaginationReached = true)
1763 ),
1764 )
1765 collectLoadStates.cancel()
1766 }
1767
1768 // TODO(b/195028524) the tests from here on checks the state after Invalid/Error results.
1769 // Upon changes due to b/195028524, the asserts on these tests should see a new resetting
1770 // LoadStateUpdate event
1771
1772 @Test fun appendInvalid_loadStates() = appendInvalid_loadStates(false)
1773
1774 @Test fun appendInvalid_loadStates_collectWithCachedIn() = appendInvalid_loadStates(true)
1775
1776 private fun appendInvalid_loadStates(collectWithCachedIn: Boolean) =
1777 runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
1778 val collectLoadStates = launch { presenter.collectLoadStates() }
1779
1780 // initial REFRESH
1781 advanceUntilIdle()
1782
1783 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1784 assertThat(presenter.newCombinedLoadStates())
1785 .containsExactly(
1786 localLoadStatesOf(refreshLocal = Loading),
1787 localLoadStatesOf(prependLocal = NotLoading.Complete),
1788 )
1789
1790 // normal append
1791 presenter[8]
1792
1793 advanceUntilIdle()
1794
1795 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
1796 assertThat(presenter.newCombinedLoadStates())
1797 .containsExactly(
1798 localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1799 localLoadStatesOf(prependLocal = NotLoading.Complete)
1800 )
1801
1802 // do invalid append which will return LoadResult.Invalid
1803 presenter[11]
1804 pagingSources[0].nextLoadResult = LoadResult.Invalid()
1805
1806 // using advanceTimeBy instead of advanceUntilIdle, otherwise this invalid APPEND +
1807 // subsequent
1808 // REFRESH will auto run consecutively and we won't be able to assert them incrementally
1809 advanceTimeBy(1001)
1810
1811 assertThat(pagingSources.size).isEqualTo(2)
1812 assertThat(presenter.newCombinedLoadStates())
1813 .containsExactly(
1814 // the invalid append
1815 localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1816 // REFRESH on new paging source. Append/Prepend local states is reset because
1817 // the
1818 // LoadStateUpdate from refresh sends the full map of a local LoadStates which
1819 // was
1820 // initialized as IDLE upon new Snapshot.
1821 localLoadStatesOf(
1822 refreshLocal = Loading,
1823 ),
1824 )
1825
1826 // the LoadResult.Invalid from failed APPEND triggers new pagingSource + initial REFRESH
1827 advanceUntilIdle()
1828
1829 assertThat(presenter.snapshot()).containsExactlyElementsIn(11 until 20)
1830 assertThat(presenter.newCombinedLoadStates())
1831 .containsExactly(
1832 localLoadStatesOf(),
1833 )
1834
1835 collectLoadStates.cancel()
1836 }
1837
1838 @Test fun appendDrop_loadStates() = appendDrop_loadStates(false)
1839
1840 @Test fun appendDrop_loadStates_collectWithCachedIn() = appendDrop_loadStates(true)
1841
1842 private fun appendDrop_loadStates(collectWithCachedIn: Boolean) =
1843 runTest(
1844 collectWithCachedIn,
1845 initialKey = 96,
1846 config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1847 ) { presenter, _, _, _ ->
1848 val collectLoadStates = launch { presenter.collectLoadStates() }
1849
1850 // initial REFRESH
1851 advanceUntilIdle()
1852
1853 assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 99)
1854 assertThat(presenter.newCombinedLoadStates())
1855 .containsExactly(
1856 localLoadStatesOf(refreshLocal = Loading),
1857 // ensure append has reached end of pagination
1858 localLoadStatesOf(),
1859 )
1860
1861 // trigger append to reach max page size
1862 presenter[2]
1863 advanceUntilIdle()
1864
1865 assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 100)
1866 assertThat(presenter.newCombinedLoadStates())
1867 .containsExactly(
1868 localLoadStatesOf(appendLocal = Loading),
1869 localLoadStatesOf(appendLocal = NotLoading.Complete),
1870 )
1871
1872 // trigger prepend and drop from append direction
1873 presenter[0]
1874 advanceUntilIdle()
1875
1876 assertThat(presenter.snapshot()).containsExactlyElementsIn(95 until 99)
1877 assertThat(presenter.newCombinedLoadStates())
1878 .containsExactly(
1879 localLoadStatesOf(prependLocal = Loading, appendLocal = NotLoading.Complete),
1880 // page from the end is dropped so now appendLocal should be
1881 // NotLoading.Incomplete
1882 localLoadStatesOf(prependLocal = Loading),
1883 localLoadStatesOf(),
1884 )
1885 collectLoadStates.cancel()
1886 }
1887
1888 @Test fun prependInvalid_loadStates() = prependInvalid_loadStates(false)
1889
1890 @Test fun prependInvalid_loadStates_collectWithCachedIn() = prependInvalid_loadStates(true)
1891
1892 private fun prependInvalid_loadStates(collectWithCachedIn: Boolean) =
1893 runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
1894 val collectLoadStates = launch { presenter.collectLoadStates() }
1895
1896 // initial REFRESH
1897 advanceUntilIdle()
1898
1899 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1900 assertThat(presenter.newCombinedLoadStates())
1901 .containsExactly(
1902 localLoadStatesOf(refreshLocal = Loading),
1903 // all local states NotLoading.Incomplete
1904 localLoadStatesOf(),
1905 )
1906
1907 // normal prepend to ensure LoadStates for Page returns remains the same
1908 presenter[0]
1909
1910 advanceUntilIdle()
1911
1912 assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
1913 assertThat(presenter.newCombinedLoadStates())
1914 .containsExactly(
1915 localLoadStatesOf(prependLocal = Loading),
1916 // all local states NotLoading.Incomplete
1917 localLoadStatesOf(),
1918 )
1919
1920 // do an invalid prepend which will return LoadResult.Invalid
1921 presenter[0]
1922 pagingSources[0].nextLoadResult = LoadResult.Invalid()
1923 advanceTimeBy(1001)
1924
1925 assertThat(pagingSources.size).isEqualTo(2)
1926 assertThat(presenter.newCombinedLoadStates())
1927 .containsExactly(
1928 // the invalid prepend
1929 localLoadStatesOf(prependLocal = Loading),
1930 // REFRESH on new paging source. Append/Prepend local states is reset because
1931 // the
1932 // LoadStateUpdate from refresh sends the full map of a local LoadStates which
1933 // was
1934 // initialized as IDLE upon new Snapshot.
1935 localLoadStatesOf(refreshLocal = Loading),
1936 )
1937
1938 // the LoadResult.Invalid from failed PREPEND triggers new pagingSource + initial
1939 // REFRESH
1940 advanceUntilIdle()
1941
1942 // load starts from 0 again because the provided initialKey = 50 is not
1943 // multi-generational
1944 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1945 assertThat(presenter.newCombinedLoadStates())
1946 .containsExactly(
1947 localLoadStatesOf(prependLocal = NotLoading.Complete),
1948 )
1949
1950 collectLoadStates.cancel()
1951 }
1952
1953 @Test fun prependDrop_loadStates() = prependDrop_loadStates(false)
1954
1955 @Test fun prependDrop_loadStates_collectWithCachedIn() = prependDrop_loadStates(true)
1956
1957 private fun prependDrop_loadStates(collectWithCachedIn: Boolean) =
1958 runTest(
1959 collectWithCachedIn,
1960 initialKey = 1,
1961 config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1962 ) { presenter, _, _, _ ->
1963 val collectLoadStates = launch { presenter.collectLoadStates() }
1964
1965 // initial REFRESH
1966 advanceUntilIdle()
1967
1968 assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 4)
1969 assertThat(presenter.newCombinedLoadStates())
1970 .containsExactly(
1971 localLoadStatesOf(refreshLocal = Loading),
1972 // ensure append has reached end of pagination
1973 localLoadStatesOf(),
1974 )
1975
1976 // trigger prepend to reach max page size
1977 presenter[0]
1978 advanceUntilIdle()
1979
1980 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 4)
1981 assertThat(presenter.newCombinedLoadStates())
1982 .containsExactly(
1983 localLoadStatesOf(prependLocal = Loading),
1984 localLoadStatesOf(prependLocal = NotLoading.Complete),
1985 )
1986
1987 // trigger append and drop from prepend direction
1988 presenter[3]
1989 advanceUntilIdle()
1990
1991 assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 5)
1992 assertThat(presenter.newCombinedLoadStates())
1993 .containsExactly(
1994 localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1995 // first page is dropped so now prependLocal should be NotLoading.Incomplete
1996 localLoadStatesOf(appendLocal = Loading),
1997 localLoadStatesOf(),
1998 )
1999
2000 collectLoadStates.cancel()
2001 }
2002
2003 @Test fun refreshInvalid_loadStates() = refreshInvalid_loadStates(false)
2004
2005 @Test fun refreshInvalid_loadStates_collectWithCachedIn() = refreshInvalid_loadStates(true)
2006
2007 private fun refreshInvalid_loadStates(collectWithCachedIn: Boolean) =
2008 runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2009 val collectLoadStates = launch { presenter.collectLoadStates() }
2010
2011 // execute queued initial REFRESH load which will return LoadResult.Invalid()
2012 pagingSources[0].nextLoadResult = LoadResult.Invalid()
2013 advanceTimeBy(1001)
2014
2015 assertThat(presenter.snapshot()).isEmpty()
2016 assertThat(presenter.newCombinedLoadStates())
2017 .containsExactly(
2018 // invalid first refresh. The second refresh state update that follows is
2019 // identical to
2020 // this LoadStates so it gets de-duped
2021 localLoadStatesOf(refreshLocal = Loading),
2022 )
2023
2024 // execute second REFRESH load
2025 advanceUntilIdle()
2026
2027 // second refresh still loads from initialKey = 50 because anchorPosition/refreshKey is
2028 // null
2029 assertThat(pagingSources.size).isEqualTo(2)
2030 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2031 assertThat(presenter.newCombinedLoadStates())
2032 .containsExactly(
2033 localLoadStatesOf(
2034 prependLocal = NotLoading.Complete,
2035 )
2036 )
2037
2038 collectLoadStates.cancel()
2039 }
2040
2041 @Test fun appendError_retryLoadStates() = appendError_retryLoadStates(false)
2042
2043 @Test fun appendError_retryLoadStates_collectWithCachedIn() = appendError_retryLoadStates(true)
2044
2045 private fun appendError_retryLoadStates(collectWithCachedIn: Boolean) =
2046 runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2047 val collectLoadStates = launch { presenter.collectLoadStates() }
2048
2049 // initial REFRESH
2050 advanceUntilIdle()
2051
2052 assertThat(presenter.newCombinedLoadStates())
2053 .containsExactly(
2054 localLoadStatesOf(refreshLocal = Loading),
2055 localLoadStatesOf(prependLocal = NotLoading.Complete),
2056 )
2057 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2058
2059 // append returns LoadResult.Error
2060 presenter[8]
2061 val exception = Throwable()
2062 pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2063
2064 advanceUntilIdle()
2065
2066 assertThat(presenter.newCombinedLoadStates())
2067 .containsExactly(
2068 localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
2069 localLoadStatesOf(
2070 prependLocal = NotLoading.Complete,
2071 appendLocal = LoadState.Error(exception)
2072 ),
2073 )
2074 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2075
2076 // retry append
2077 presenter.retry()
2078 advanceUntilIdle()
2079
2080 // make sure append success
2081 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
2082 // no reset
2083 assertThat(presenter.newCombinedLoadStates())
2084 .containsExactly(
2085 localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
2086 localLoadStatesOf(prependLocal = NotLoading.Complete),
2087 )
2088
2089 collectLoadStates.cancel()
2090 }
2091
2092 @Test fun prependError_retryLoadStates() = prependError_retryLoadStates(false)
2093
2094 @Test
2095 fun prependError_retryLoadStates_collectWithCachedIn() = prependError_retryLoadStates(true)
2096
2097 private fun prependError_retryLoadStates(collectWithCachedIn: Boolean) =
2098 runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2099 val collectLoadStates = launch { presenter.collectLoadStates() }
2100
2101 // initial REFRESH
2102 advanceUntilIdle()
2103
2104 assertThat(presenter.newCombinedLoadStates())
2105 .containsExactly(
2106 localLoadStatesOf(refreshLocal = Loading),
2107 localLoadStatesOf(),
2108 )
2109
2110 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2111
2112 // prepend returns LoadResult.Error
2113 presenter[0]
2114 val exception = Throwable()
2115 pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2116
2117 advanceUntilIdle()
2118
2119 assertThat(presenter.newCombinedLoadStates())
2120 .containsExactly(
2121 localLoadStatesOf(prependLocal = Loading),
2122 localLoadStatesOf(prependLocal = LoadState.Error(exception)),
2123 )
2124 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2125
2126 // retry prepend
2127 presenter.retry()
2128
2129 advanceUntilIdle()
2130
2131 // make sure prepend success
2132 assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
2133 assertThat(presenter.newCombinedLoadStates())
2134 .containsExactly(
2135 localLoadStatesOf(prependLocal = Loading),
2136 localLoadStatesOf(),
2137 )
2138
2139 collectLoadStates.cancel()
2140 }
2141
2142 @Test fun refreshError_retryLoadStates() = refreshError_retryLoadStates(false)
2143
2144 @Test
2145 fun refreshError_retryLoadStates_collectWithCachedIn() = refreshError_retryLoadStates(true)
2146
2147 private fun refreshError_retryLoadStates(collectWithCachedIn: Boolean) =
2148 runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2149 val collectLoadStates = launch { presenter.collectLoadStates() }
2150
2151 // initial load returns LoadResult.Error
2152 val exception = Throwable()
2153 pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2154
2155 advanceUntilIdle()
2156
2157 assertThat(presenter.newCombinedLoadStates())
2158 .containsExactly(
2159 localLoadStatesOf(refreshLocal = Loading),
2160 localLoadStatesOf(refreshLocal = LoadState.Error(exception)),
2161 )
2162 assertThat(presenter.snapshot()).isEmpty()
2163
2164 // retry refresh
2165 presenter.retry()
2166
2167 advanceUntilIdle()
2168
2169 // refresh retry does not trigger new gen
2170 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2171 // Goes directly from Error --> Loading without resetting refresh to NotLoading
2172 assertThat(presenter.newCombinedLoadStates())
2173 .containsExactly(
2174 localLoadStatesOf(refreshLocal = Loading),
2175 localLoadStatesOf(prependLocal = NotLoading.Complete),
2176 )
2177
2178 collectLoadStates.cancel()
2179 }
2180
2181 @Test fun prependError_refreshLoadStates() = prependError_refreshLoadStates(false)
2182
2183 @Test
2184 fun prependError_refreshLoadStates_collectWithCachedIn() = prependError_refreshLoadStates(true)
2185
2186 private fun prependError_refreshLoadStates(collectWithCachedIn: Boolean) =
2187 runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2188 val collectLoadStates = launch { presenter.collectLoadStates() }
2189
2190 // initial REFRESH
2191 advanceUntilIdle()
2192
2193 assertThat(presenter.newCombinedLoadStates())
2194 .containsExactly(
2195 localLoadStatesOf(refreshLocal = Loading),
2196 localLoadStatesOf(),
2197 )
2198 assertThat(presenter.size).isEqualTo(9)
2199 assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2200
2201 // prepend returns LoadResult.Error
2202 presenter[0]
2203 val exception = Throwable()
2204 pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2205
2206 advanceUntilIdle()
2207
2208 assertThat(presenter.newCombinedLoadStates())
2209 .containsExactly(
2210 localLoadStatesOf(prependLocal = Loading),
2211 localLoadStatesOf(prependLocal = LoadState.Error(exception)),
2212 )
2213
2214 // refresh() should reset local LoadStates and trigger new REFRESH
2215 presenter.refresh()
2216 advanceUntilIdle()
2217
2218 // Initial load starts from 0 because initialKey is single gen.
2219 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2220 assertThat(presenter.newCombinedLoadStates())
2221 .containsExactly(
2222 // second gen REFRESH load. The Error prepend state was automatically reset to
2223 // NotLoading.
2224 localLoadStatesOf(refreshLocal = Loading),
2225 // REFRESH complete
2226 localLoadStatesOf(prependLocal = NotLoading.Complete),
2227 )
2228
2229 collectLoadStates.cancel()
2230 }
2231
2232 @Test fun refreshError_refreshLoadStates() = refreshError_refreshLoadStates(false)
2233
2234 @Test
2235 fun refreshError_refreshLoadStates_collectWithCachedIn() = refreshError_refreshLoadStates(true)
2236
2237 private fun refreshError_refreshLoadStates(collectWithCachedIn: Boolean) =
2238 runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2239 val collectLoadStates = launch { presenter.collectLoadStates() }
2240
2241 // the initial load will return LoadResult.Error
2242 val exception = Throwable()
2243 pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2244
2245 advanceUntilIdle()
2246
2247 assertThat(presenter.newCombinedLoadStates())
2248 .containsExactly(
2249 localLoadStatesOf(refreshLocal = Loading),
2250 localLoadStatesOf(refreshLocal = LoadState.Error(exception)),
2251 )
2252 assertThat(presenter.snapshot()).isEmpty()
2253
2254 // refresh should trigger new generation
2255 presenter.refresh()
2256
2257 advanceUntilIdle()
2258
2259 assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2260 // Goes directly from Error --> Loading without resetting refresh to NotLoading
2261 assertThat(presenter.newCombinedLoadStates())
2262 .containsExactly(
2263 localLoadStatesOf(refreshLocal = Loading),
2264 localLoadStatesOf(prependLocal = NotLoading.Complete),
2265 )
2266
2267 collectLoadStates.cancel()
2268 }
2269
2270 @Test
2271 fun remoteRefresh_refreshStatePersists() =
2272 testScope.runTest {
2273 val presenter = SimplePresenter()
2274 val remoteMediator =
2275 RemoteMediatorMock(loadDelay = 1500).apply {
2276 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
2277 }
2278 val pager =
2279 Pager(
2280 PagingConfig(pageSize = 3, enablePlaceholders = false),
2281 remoteMediator = remoteMediator,
2282 ) {
2283 TestPagingSource(loadDelay = 500, items = emptyList())
2284 }
2285
2286 val collectLoadStates = launch { presenter.collectLoadStates() }
2287 val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
2288 // allow local refresh to complete but not remote refresh
2289 advanceTimeBy(600)
2290
2291 assertThat(presenter.newCombinedLoadStates())
2292 .containsExactly(
2293 // local starts loading
2294 remoteLoadStatesOf(
2295 refreshLocal = Loading,
2296 ),
2297 // remote starts loading
2298 remoteLoadStatesOf(
2299 refresh = Loading,
2300 refreshLocal = Loading,
2301 refreshRemote = Loading,
2302 ),
2303 // local load returns with empty data, mediator is still loading
2304 remoteLoadStatesOf(
2305 refresh = Loading,
2306 prependLocal = NotLoading.Complete,
2307 appendLocal = NotLoading.Complete,
2308 refreshRemote = Loading,
2309 ),
2310 )
2311
2312 // refresh triggers new generation & LoadState reset
2313 presenter.refresh()
2314
2315 // allow local refresh to complete but not remote refresh
2316 advanceTimeBy(600)
2317
2318 assertThat(presenter.newCombinedLoadStates())
2319 .containsExactly(
2320 // local starts second refresh while mediator continues remote refresh from
2321 // before
2322 remoteLoadStatesOf(
2323 refresh = Loading,
2324 refreshLocal = Loading,
2325 refreshRemote = Loading,
2326 ),
2327 // local load returns empty data
2328 remoteLoadStatesOf(
2329 refresh = Loading,
2330 prependLocal = NotLoading.Complete,
2331 appendLocal = NotLoading.Complete,
2332 refreshRemote = Loading,
2333 ),
2334 )
2335
2336 // allow remote refresh to complete
2337 advanceTimeBy(600)
2338
2339 assertThat(presenter.newCombinedLoadStates())
2340 .containsExactly(
2341 // remote refresh returns empty and triggers remote append/prepend
2342 remoteLoadStatesOf(
2343 prepend = Loading,
2344 append = Loading,
2345 prependLocal = NotLoading.Complete,
2346 appendLocal = NotLoading.Complete,
2347 prependRemote = Loading,
2348 appendRemote = Loading,
2349 ),
2350 )
2351
2352 // allow remote append and prepend to complete
2353 advanceUntilIdle()
2354
2355 assertThat(presenter.newCombinedLoadStates())
2356 .containsExactly(
2357 // prepend completes first
2358 remoteLoadStatesOf(
2359 append = Loading,
2360 prependLocal = NotLoading.Complete,
2361 appendLocal = NotLoading.Complete,
2362 appendRemote = Loading,
2363 ),
2364 remoteLoadStatesOf(
2365 prependLocal = NotLoading.Complete,
2366 appendLocal = NotLoading.Complete,
2367 ),
2368 )
2369
2370 job.cancel()
2371 collectLoadStates.cancel()
2372 }
2373
2374 @Test
2375 fun recollectOnNewPresenter_initialLoadStates() =
2376 testScope.runTest {
2377 val pager =
2378 Pager(
2379 config = PagingConfig(pageSize = 3, enablePlaceholders = false),
2380 initialKey = 50,
2381 pagingSourceFactory = { TestPagingSource() }
2382 )
2383 .flow
2384 .cachedIn(this)
2385
2386 val presenter = SimplePresenter()
2387 backgroundScope.launch { presenter.collectLoadStates() }
2388
2389 val job = launch { pager.collectLatest { presenter.collectFrom(it) } }
2390 advanceUntilIdle()
2391
2392 assertThat(presenter.newCombinedLoadStates())
2393 .containsExactly(localLoadStatesOf(refreshLocal = Loading), localLoadStatesOf())
2394
2395 // we start a separate presenter to recollect on cached Pager.flow
2396 val presenter2 = SimplePresenter()
2397 backgroundScope.launch { presenter2.collectLoadStates() }
2398
2399 val job2 = launch { pager.collectLatest { presenter2.collectFrom(it) } }
2400 advanceUntilIdle()
2401
2402 assertThat(presenter2.newCombinedLoadStates()).containsExactly(localLoadStatesOf())
2403
2404 job.cancel()
2405 job2.cancel()
2406 coroutineContext.cancelChildren()
2407 }
2408
2409 @Test
2410 fun cachedData() {
2411 val data = List(50) { it }
2412 val cachedPagingData = createCachedPagingData(data)
2413 val simplePresenter = SimplePresenter(cachedPagingData)
2414 assertThat(simplePresenter.snapshot()).isEqualTo(data)
2415 assertThat(simplePresenter.size).isEqualTo(data.size)
2416 }
2417
2418 @Test
2419 fun emptyCachedData() {
2420 val cachedPagingData = createCachedPagingData(emptyList())
2421 val simplePresenter = SimplePresenter(cachedPagingData)
2422 assertThat(simplePresenter.snapshot()).isEmpty()
2423 assertThat(simplePresenter.size).isEqualTo(0)
2424 }
2425
2426 @Test
2427 fun cachedLoadStates() {
2428 val data = List(50) { it }
2429 val localStates = loadStates(refresh = Loading)
2430 val mediatorStates = loadStates()
2431 val cachedPagingData =
2432 createCachedPagingData(
2433 data = data,
2434 sourceLoadStates = localStates,
2435 mediatorLoadStates = mediatorStates
2436 )
2437 val simplePresenter = SimplePresenter(cachedPagingData)
2438 val expected = simplePresenter.loadStateFlow.value
2439 assertThat(expected).isNotNull()
2440 assertThat(expected!!.source).isEqualTo(localStates)
2441 assertThat(expected.mediator).isEqualTo(mediatorStates)
2442 }
2443
2444 @Test
2445 fun cachedData_doesNotSetHintReceiver() =
2446 testScope.runTest {
2447 val data = List(50) { it }
2448 val hintReceiver = HintReceiverFake()
2449 val cachedPagingData =
2450 createCachedPagingData(
2451 data = data,
2452 sourceLoadStates = loadStates(refresh = Loading),
2453 mediatorLoadStates = null,
2454 hintReceiver = hintReceiver
2455 )
2456 val presenter = SimplePresenter(cachedPagingData)
2457
2458 // access item
2459 presenter[5]
2460 assertThat(hintReceiver.hints).hasSize(0)
2461
2462 val flow =
2463 flowOf(
2464 localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
2465 )
2466 val hintReceiver2 = HintReceiverFake()
2467
2468 val job1 = launch {
2469 presenter.collectFrom(PagingData(flow, dummyUiReceiver, hintReceiver2))
2470 }
2471
2472 // access item, hint should be sent to the first uncached PagingData
2473 presenter[3]
2474 assertThat(hintReceiver.hints).hasSize(0)
2475 assertThat(hintReceiver2.hints).hasSize(1)
2476 job1.cancel()
2477 }
2478
2479 @Test
2480 fun cachedData_doesNotSetUiReceiver() =
2481 testScope.runTest {
2482 val data = List(50) { it }
2483 val uiReceiver = UiReceiverFake()
2484 val cachedPagingData =
2485 createCachedPagingData(
2486 data = data,
2487 sourceLoadStates = loadStates(refresh = Loading),
2488 mediatorLoadStates = null,
2489 uiReceiver = uiReceiver
2490 )
2491 val presenter = SimplePresenter(cachedPagingData)
2492 presenter.refresh()
2493 advanceUntilIdle()
2494 assertThat(uiReceiver.refreshEvents).hasSize(0)
2495
2496 val flow =
2497 flowOf(
2498 localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
2499 )
2500 val uiReceiver2 = UiReceiverFake()
2501 val job1 = launch {
2502 presenter.collectFrom(PagingData(flow, uiReceiver2, dummyHintReceiver))
2503 }
2504 assertThat(uiReceiver.refreshEvents).hasSize(0)
2505 assertThat(uiReceiver2.refreshEvents).hasSize(1)
2506 job1.cancel()
2507 }
2508
2509 @Test
2510 fun cachedData_thenRealData() =
2511 testScope.runTest {
2512 val data = List(2) { it }
2513 val cachedPagingData =
2514 createCachedPagingData(
2515 data = data,
2516 sourceLoadStates = loadStates(refresh = Loading),
2517 mediatorLoadStates = null,
2518 )
2519 val presenter = SimplePresenter(cachedPagingData)
2520 val data2 = List(10) { it }
2521 val flow =
2522 flowOf(
2523 localRefresh(pages = listOf(TransformablePage(data2))),
2524 )
2525 val job1 = launch {
2526 presenter.collectFrom(PagingData(flow, dummyUiReceiver, dummyHintReceiver))
2527 }
2528
2529 assertThat(presenter.snapshot()).isEqualTo(data2)
2530 job1.cancel()
2531 }
2532
2533 @Test
2534 fun cachedData_thenLoadError() =
2535 testScope.runTest {
2536 val data = List(3) { it }
2537 val cachedPagingData =
2538 createCachedPagingData(
2539 data = data,
2540 sourceLoadStates = loadStates(refresh = Loading),
2541 mediatorLoadStates = null,
2542 )
2543 val presenter = SimplePresenter(cachedPagingData)
2544
2545 val channel = Channel<PageEvent<Int>>(Channel.UNLIMITED)
2546 val hintReceiver = HintReceiverFake()
2547 val uiReceiver = UiReceiverFake()
2548 val job1 = launch {
2549 presenter.collectFrom(PagingData(channel.consumeAsFlow(), uiReceiver, hintReceiver))
2550 }
2551 val error = LoadState.Error(Exception())
2552 channel.trySend(localLoadStateUpdate(refreshLocal = error))
2553 assertThat(presenter.nonNullLoadStateFlow.first())
2554 .isEqualTo(localLoadStatesOf(refreshLocal = error))
2555
2556 // ui receiver is set upon processing a LoadStateUpdate so we can still trigger
2557 // refresh/retry
2558 presenter.refresh()
2559 assertThat(uiReceiver.refreshEvents).hasSize(1)
2560 // but hint receiver is only set if presenter has presented a refresh from this
2561 // PagingData
2562 // which did not happen in this case
2563 presenter[2]
2564 assertThat(hintReceiver.hints).hasSize(0)
2565 job1.cancel()
2566 }
2567
2568 private fun runTest(
2569 collectWithCachedIn: Boolean,
2570 initialKey: Int? = null,
2571 config: PagingConfig = PagingConfig(pageSize = 3, enablePlaceholders = false),
2572 block:
2573 TestScope.(
2574 presenter: SimplePresenter,
2575 pagingSources: List<TestPagingSource>,
2576 uiReceivers: List<TrackableUiReceiverWrapper>,
2577 hintReceivers: List<TrackableHintReceiverWrapper>
2578 ) -> Unit
2579 ) =
2580 testScope.runTest {
2581 val pagingSources = mutableListOf<TestPagingSource>()
2582 val pager =
2583 Pager(
2584 config = config,
2585 initialKey = initialKey,
2586 pagingSourceFactory = {
2587 TestPagingSource(
2588 loadDelay = 1000,
2589 )
2590 .also { pagingSources.add(it) }
2591 }
2592 )
2593 val presenter = SimplePresenter()
2594 val uiReceivers = mutableListOf<TrackableUiReceiverWrapper>()
2595 val hintReceivers = mutableListOf<TrackableHintReceiverWrapper>()
2596
2597 val collection = launch {
2598 pager.flow
2599 .map { pagingData ->
2600 PagingData(
2601 flow = pagingData.flow,
2602 uiReceiver =
2603 TrackableUiReceiverWrapper(pagingData.uiReceiver).also {
2604 uiReceivers.add(it)
2605 },
2606 hintReceiver =
2607 TrackableHintReceiverWrapper(pagingData.hintReceiver).also {
2608 hintReceivers.add(it)
2609 }
2610 )
2611 }
2612 .let {
2613 if (collectWithCachedIn) {
2614 it.cachedIn(this)
2615 } else {
2616 it
2617 }
2618 }
2619 .collect { presenter.collectFrom(it) }
2620 }
2621
2622 try {
2623 block(presenter, pagingSources, uiReceivers, hintReceivers)
2624 } finally {
2625 collection.cancel()
2626 }
2627 }
2628 }
2629
infinitelySuspendingPagingDatanull2630 private fun infinitelySuspendingPagingData(
2631 uiReceiver: UiReceiver = dummyUiReceiver,
2632 hintReceiver: HintReceiver = dummyHintReceiver
2633 ) =
2634 PagingData(
2635 flow { emit(suspendCancellableCoroutine<PageEvent<Int>> {}) },
2636 uiReceiver,
2637 hintReceiver
2638 )
2639
createCachedPagingDatanull2640 private fun createCachedPagingData(
2641 data: List<Int>,
2642 placeholdersBefore: Int = 0,
2643 placeholdersAfter: Int = 0,
2644 uiReceiver: UiReceiver = PagingData.NOOP_UI_RECEIVER,
2645 hintReceiver: HintReceiver = PagingData.NOOP_HINT_RECEIVER,
2646 sourceLoadStates: LoadStates = LoadStates.IDLE,
2647 mediatorLoadStates: LoadStates? = null,
2648 ): PagingData<Int> =
2649 PagingData(
2650 flow = emptyFlow(),
2651 uiReceiver = uiReceiver,
2652 hintReceiver = hintReceiver,
2653 cachedPageEvent = {
2654 PageEvent.Insert.Refresh(
2655 pages = listOf(TransformablePage(0, data)),
2656 placeholdersBefore = placeholdersBefore,
2657 placeholdersAfter = placeholdersAfter,
2658 sourceLoadStates = sourceLoadStates,
2659 mediatorLoadStates = mediatorLoadStates
2660 )
2661 }
2662 )
2663
2664 private class UiReceiverFake : UiReceiver {
2665 val retryEvents = mutableListOf<Unit>()
2666 val refreshEvents = mutableListOf<Unit>()
2667
retrynull2668 override fun retry() {
2669 retryEvents.add(Unit)
2670 }
2671
refreshnull2672 override fun refresh() {
2673 refreshEvents.add(Unit)
2674 }
2675 }
2676
2677 private class HintReceiverFake : HintReceiver {
2678 private val _hints = mutableListOf<ViewportHint>()
2679 val hints: List<ViewportHint>
2680 get() {
2681 val result = _hints.toList()
<lambda>null2682 @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }
2683 return result
2684 }
2685
accessHintnull2686 override fun accessHint(viewportHint: ViewportHint) {
2687 _hints.add(viewportHint)
2688 }
2689 }
2690
2691 private class TrackableUiReceiverWrapper(
2692 private val receiver: UiReceiver? = null,
2693 ) : UiReceiver {
2694 val retryEvents = mutableListOf<Unit>()
2695 val refreshEvents = mutableListOf<Unit>()
2696
retrynull2697 override fun retry() {
2698 retryEvents.add(Unit)
2699 receiver?.retry()
2700 }
2701
refreshnull2702 override fun refresh() {
2703 refreshEvents.add(Unit)
2704 receiver?.refresh()
2705 }
2706 }
2707
2708 private class TrackableHintReceiverWrapper(
2709 private val receiver: HintReceiver? = null,
2710 ) : HintReceiver {
2711 private val _hints = mutableListOf<ViewportHint>()
2712 val hints: List<ViewportHint>
2713 get() {
2714 val result = _hints.toList()
<lambda>null2715 @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }
2716 return result
2717 }
2718
accessHintnull2719 override fun accessHint(viewportHint: ViewportHint) {
2720 _hints.add(viewportHint)
2721 receiver?.accessHint(viewportHint)
2722 }
2723 }
2724
2725 private class SimplePresenter(
2726 cachedPagingData: PagingData<Int>? = null,
2727 ) :
2728 PagingDataPresenter<Int>(
2729 mainContext = EmptyCoroutineContext,
2730 cachedPagingData = cachedPagingData
2731 ) {
2732 private val _localLoadStates = mutableListOf<CombinedLoadStates>()
2733
2734 val nonNullLoadStateFlow = loadStateFlow.filterNotNull()
2735
newCombinedLoadStatesnull2736 fun newCombinedLoadStates(): List<CombinedLoadStates?> {
2737 val newCombinedLoadStates = _localLoadStates.toList()
2738 _localLoadStates.clear()
2739 return newCombinedLoadStates
2740 }
2741
collectLoadStatesnull2742 suspend fun collectLoadStates() {
2743 nonNullLoadStateFlow.collect { combinedLoadStates ->
2744 _localLoadStates.add(combinedLoadStates)
2745 }
2746 }
2747
2748 private val _pagingDataEvents = mutableListOf<PagingDataEvent<Int>>()
2749
newEventsnull2750 fun newEvents(): List<PagingDataEvent<Int>> {
2751 val newEvents = _pagingDataEvents.toList()
2752 _pagingDataEvents.clear()
2753 return newEvents
2754 }
2755
presentPagingDataEventnull2756 override suspend fun presentPagingDataEvent(event: PagingDataEvent<Int>) {
2757 _pagingDataEvents.add(event)
2758 }
2759 }
2760
2761 internal val dummyUiReceiver =
2762 object : UiReceiver {
retrynull2763 override fun retry() {}
2764
refreshnull2765 override fun refresh() {}
2766 }
2767
2768 internal val dummyHintReceiver =
2769 object : HintReceiver {
accessHintnull2770 override fun accessHint(viewportHint: ViewportHint) {}
2771 }
2772