1 /*
<lambda>null2  * Copyright 2019 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 @file:Suppress("DEPRECATION") // b/220884819
18 
19 package androidx.paging
20 
21 import android.view.View
22 import android.view.ViewGroup
23 import androidx.arch.core.executor.ArchTaskExecutor
24 import androidx.arch.core.executor.testing.InstantTaskExecutorRule
25 import androidx.recyclerview.widget.DiffUtil
26 import androidx.recyclerview.widget.RecyclerView
27 import androidx.test.ext.junit.runners.AndroidJUnit4
28 import androidx.test.filters.SmallTest
29 import androidx.testutils.TestDispatcher
30 import com.google.common.truth.Truth.assertThat
31 import kotlin.test.assertEquals
32 import kotlin.test.assertTrue
33 import kotlinx.coroutines.CoroutineDispatcher
34 import kotlinx.coroutines.DelicateCoroutinesApi
35 import kotlinx.coroutines.Dispatchers
36 import kotlinx.coroutines.ExperimentalCoroutinesApi
37 import kotlinx.coroutines.GlobalScope
38 import kotlinx.coroutines.asCoroutineDispatcher
39 import kotlinx.coroutines.test.TestCoroutineScope
40 import kotlinx.coroutines.test.advanceUntilIdle
41 import kotlinx.coroutines.test.runBlockingTest
42 import org.junit.Assert.assertNotNull
43 import org.junit.Rule
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 
47 @RunWith(AndroidJUnit4::class)
48 @SmallTest
49 @Suppress("DEPRECATION")
50 @OptIn(ExperimentalCoroutinesApi::class)
51 class LivePagedListTest {
52     @JvmField @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
53 
54     private val testScope = TestCoroutineScope()
55 
56     @OptIn(DelicateCoroutinesApi::class)
57     @Test
58     fun invalidPagingSourceOnInitialLoadTriggersInvalidation() {
59         var pagingSourcesCreated = 0
60         val pagingSourceFactory = {
61             when (pagingSourcesCreated++) {
62                 0 -> TestPagingSource().apply { invalidate() }
63                 else -> TestPagingSource()
64             }
65         }
66 
67         val livePagedList =
68             LivePagedList(
69                 coroutineScope = GlobalScope,
70                 initialKey = null,
71                 config = PagedList.Config.Builder().setPageSize(10).build(),
72                 boundaryCallback = null,
73                 pagingSourceFactory = pagingSourceFactory,
74                 notifyDispatcher = ArchTaskExecutor.getMainThreadExecutor().asCoroutineDispatcher(),
75                 fetchDispatcher = ArchTaskExecutor.getIOThreadExecutor().asCoroutineDispatcher(),
76             )
77 
78         livePagedList.observeForever {}
79         assertThat(pagingSourcesCreated).isEqualTo(2)
80     }
81 
82     @OptIn(DelicateCoroutinesApi::class)
83     @Test
84     fun instantiatesPagingSourceOnFetchDispatcher() {
85         var pagingSourcesCreated = 0
86         val pagingSourceFactory = {
87             pagingSourcesCreated++
88             TestPagingSource()
89         }
90         val testDispatcher = TestDispatcher()
91         val livePagedList =
92             LivePagedList(
93                 coroutineScope = GlobalScope,
94                 initialKey = null,
95                 config = PagedList.Config.Builder().setPageSize(10).build(),
96                 boundaryCallback = null,
97                 pagingSourceFactory = pagingSourceFactory,
98                 notifyDispatcher = ArchTaskExecutor.getMainThreadExecutor().asCoroutineDispatcher(),
99                 fetchDispatcher = testDispatcher,
100             )
101 
102         assertTrue { testDispatcher.queue.isEmpty() }
103         assertEquals(0, pagingSourcesCreated)
104 
105         livePagedList.observeForever {}
106 
107         assertTrue { testDispatcher.queue.isNotEmpty() }
108         assertEquals(0, pagingSourcesCreated)
109 
110         testDispatcher.executeAll()
111         assertEquals(1, pagingSourcesCreated)
112     }
113 
114     @Test
115     fun toLiveData_dataSourceConfig() {
116         val livePagedList = dataSourceFactory.toLiveData(config)
117         livePagedList.observeForever {}
118         assertNotNull(livePagedList.value)
119         assertEquals(config, livePagedList.value!!.config)
120     }
121 
122     @Test
123     fun toLiveData_dataSourcePageSize() {
124         val livePagedList = dataSourceFactory.toLiveData(24)
125         livePagedList.observeForever {}
126         assertNotNull(livePagedList.value)
127         assertEquals(24, livePagedList.value!!.config.pageSize)
128     }
129 
130     @Test
131     fun toLiveData_pagingSourceConfig() {
132         val livePagedList = pagingSourceFactory.toLiveData(config)
133         livePagedList.observeForever {}
134         assertNotNull(livePagedList.value)
135         assertEquals(config, livePagedList.value!!.config)
136     }
137 
138     @Test
139     fun toLiveData_pagingSourcePageSize() {
140         val livePagedList = pagingSourceFactory.toLiveData(24)
141         livePagedList.observeForever {}
142         assertNotNull(livePagedList.value)
143         assertEquals(24, livePagedList.value!!.config.pageSize)
144     }
145 
146     /**
147      * Some paging2 tests might be using InstantTaskExecutor and expect first page to be loaded
148      * immediately. This test replicates that by checking observe forever receives the value in its
149      * own call stack.
150      */
151     @Test
152     fun instantExecutionWorksWithLegacy() {
153         val totalSize = 300
154         val data = (0 until totalSize).map { "$it/$it" }
155         val factory =
156             object : DataSource.Factory<Int, String>() {
157                 override fun create(): DataSource<Int, String> {
158                     return TestPositionalDataSource(data)
159                 }
160             }
161 
162         class TestAdapter : PagedListAdapter<String, RecyclerView.ViewHolder>(DIFF_STRING) {
163             // open it up by overriding
164             public override fun getItem(position: Int): String? {
165                 return super.getItem(position)
166             }
167 
168             override fun onCreateViewHolder(
169                 parent: ViewGroup,
170                 viewType: Int
171             ): RecyclerView.ViewHolder {
172                 return object : RecyclerView.ViewHolder(View(parent.context)) {}
173             }
174 
175             override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
176         }
177 
178         val livePagedList =
179             LivePagedListBuilder(
180                     factory,
181                     PagedList.Config.Builder().setEnablePlaceholders(false).setPageSize(30).build()
182                 )
183                 .build()
184 
185         val adapter = TestAdapter()
186         livePagedList.observeForever { pagedList ->
187             // make sure observeForever worked sync where it did load the data immediately
188             assertThat(Throwable().stackTraceToString()).contains("observeForever")
189             assertThat(pagedList.loadedCount).isEqualTo(90)
190         }
191         adapter.submitList(checkNotNull(livePagedList.value))
192         assertThat(adapter.itemCount).isEqualTo(90)
193 
194         (0 until totalSize).forEach {
195             // getting that item will trigger load around which should load the item immediately
196             assertThat(adapter.getItem(it)).isEqualTo("$it/$it")
197         }
198     }
199 
200     @OptIn(ExperimentalStdlibApi::class)
201     @Test
202     fun initialLoad_loadResultInvalid() =
203         testScope.runBlockingTest {
204             val dispatcher = coroutineContext[CoroutineDispatcher.Key]!!
205             val pagingSources = mutableListOf<TestPagingSource>()
206             val factory = {
207                 TestPagingSource().also {
208                     if (pagingSources.size == 0)
209                         it.nextLoadResult = PagingSource.LoadResult.Invalid()
210                     pagingSources.add(it)
211                 }
212             }
213             val config =
214                 PagedList.Config.Builder().setEnablePlaceholders(false).setPageSize(3).build()
215 
216             val livePagedList =
217                 LivePagedList(
218                     coroutineScope = testScope,
219                     initialKey = null,
220                     config = config,
221                     boundaryCallback = null,
222                     pagingSourceFactory = factory,
223                     notifyDispatcher = dispatcher,
224                     fetchDispatcher = dispatcher,
225                 )
226 
227             val pagedLists = mutableListOf<PagedList<Int>>()
228             livePagedList.observeForever { pagedLists.add(it) }
229 
230             advanceUntilIdle()
231 
232             assertThat(pagedLists.size).isEqualTo(2)
233             assertThat(pagingSources.size).isEqualTo(2)
234             assertThat(pagedLists.size).isEqualTo(2)
235             assertThat(pagedLists[1]).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8)
236         }
237 
238     companion object {
239         @Suppress("DEPRECATION")
240         private val dataSource =
241             object : PositionalDataSource<String>() {
242                 override fun loadInitial(
243                     params: LoadInitialParams,
244                     callback: LoadInitialCallback<String>
245                 ) {}
246 
247                 override fun loadRange(
248                     params: LoadRangeParams,
249                     callback: LoadRangeCallback<String>
250                 ) {}
251             }
252 
253         private val dataSourceFactory =
254             object : DataSource.Factory<Int, String>() {
255                 override fun create(): DataSource<Int, String> = dataSource
256             }
257 
258         private val pagingSourceFactory =
259             dataSourceFactory.asPagingSourceFactory(fetchDispatcher = Dispatchers.Main)
260 
261         private val config = Config(10)
262         private val DIFF_STRING =
263             object : DiffUtil.ItemCallback<String>() {
264                 override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
265                     return oldItem == newItem
266                 }
267 
268                 override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
269                     return oldItem == newItem
270                 }
271             }
272     }
273 }
274