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