Paging 3
- Android Jetpack에서 제공하는 페이징을 위한 라이브러리입니다.
- 성능, 메모리, 비용 측면에서 효율적입니다.
PagingSource
- 네트워크 또는 데이터 베이스에서 데이터를 로드하는 추상 클래스. key 타입을 정의하여 구현합니다.
- LoadResult라는 sealed class에서 응답 처리와 에러 핸들링에 도움을 주기 때문에, 자체적으로 결과 클래스를 만들어 래핑 할 필요가 없습니다.
- 로드할 다음 페이지가 없으면 null을 nextKey에 전달, 이전 페이지가 없으면 prevKey에 전달합니다.
class PicsumPagingSource(private val service: PicsumService) : PagingSource<Int, Item>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
return try {
val page = params.key ?: 0
val items = service.getImages(page, 10)
LoadResult.Page(
data = items,
prevKey = if (page == 0) null else page - 1,
nextKey = if (items.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
- Paging Source를 테스트를 하기 전에 필요한 코드들을 선언해 줬습니다.
- InstantTaskExecutorRule은 작업을 동일한 스레드에서 동기적으로 처리하게 해줍니다.
- 코루틴을 사용하기 때문에 TestCoroutineDispathcer를 사용합니다.
@ExperimentalCoroutinesApi
class PicsumPagingSourceTest {
@get:Rule
val mainCoroutineRule = MainCoroutineRule()
@get:Rule
val instanceExecutorRule = InstantTaskExecutorRule()
private val service: PicsumService = mockk()
private lateinit var picsumPagingSource: PicsumPagingSource
@Before
fun setUp() {
picsumPagingSource = PicsumPagingSource(service)
}
- 이벤트를 발생시키는 key는 LoadPrarms입니다. LoadPrarms는 seald class이며 3 가지의 타입이 있습니다.
- Refersh - 초기 로드 또는 새로고침.
- Append - 데이터 페이지를 로드하고 목록의 끝에 추가.
- Prepend - 데이터 페이지를 로드하고 목록의 시작 부분에 추가.
Paging Source - failure / error 테스트
@Test
fun `paging source load failure received io exception`() = mainCoroutineRule.runBlockingTest {
val error = IOException("404", Throwable())
coEvery { service.getImages(any(), any()) } throws error
val expectedResult = PagingSource.LoadResult.Error<Int, Item>(error)
Assert.assertEquals(
expectedResult, picsumPagingSource.load(
PagingSource.LoadParams.Refresh(
key = 0,
loadSize = 1,
placeholdersEnabled = false
)
)
)
}
@Test
fun `paging source load failure received null exception`() = mainCoroutineRule.runBlockingTest {
coEvery { service.getImages(any(), any()) } throws NullPointerException()
val expectedResult = PagingSource.LoadResult.Error<Int, Item>(NullPointerException())
Assert.assertEquals(
expectedResult.toString(), picsumPagingSource.load(
PagingSource.LoadParams.Refresh(
key = 0,
loadSize = 1,
placeholdersEnabled = false
)
).toString()
)
}
Paigng Source - refresh 테스트
@Test
fun `paging source refresh received success`() = mainCoroutineRule.runBlockingTest {
coEvery { service.getImages(any(), any()) } returns mockItemList
service.getImages(0, 10)
coVerify { service.getImages(any(), any()) }
val expectedResult =
PagingSource.LoadResult.Page(data = mockItemList, prevKey = null, nextKey = 1)
Assert.assertEquals(
expectedResult, picsumPagingSource.load(
PagingSource.LoadParams.Refresh(
key = 0,
loadSize = 10,
placeholdersEnabled = false
)
)
)
}
Paigng Source - append 테스트
@Test
fun `paging source append received success`() = mainCoroutineRule.runBlockingTest {
coEvery { service.getImages(any(), any()) } returns mockItemList
service.getImages(0, 10)
coVerify { service.getImages(any(), any()) }
val expectedResult =
PagingSource.LoadResult.Page(data = mockItemList, prevKey = 0, nextKey = 2)
Assert.assertEquals(
expectedResult, picsumPagingSource.load(
PagingSource.LoadParams.Append(
key = 1,
loadSize = 10,
placeholdersEnabled = false
)
)
)
}
Paging Source - prepend 테스트
@Test
fun `paging source prepend received success`() = mainCoroutineRule.runBlockingTest {
coEvery { service.getImages(any(), any()) } returns mockItemList
service.getImages(0, 10)
coVerify { service.getImages(any(), any()) }
val expectedResult =
PagingSource.LoadResult.Page(data = mockItemList, prevKey = null, nextKey = 1)
Assert.assertEquals(
expectedResult, picsumPagingSource.load(
PagingSource.LoadParams.Prepend(
key = 0,
loadSize = 10,
placeholdersEnabled = false
)
)
)
}
RemoteMediator
- 네트워크 및 로컬 데이터베이스에서 페이징 데이터를 로드하는 역할.
- 로컬 데이터베이스를 데이터 소스로 활용함으로써, 네트워크 연결이 불안정할 시 좋은 방법.
- load() 메서드가 정확한 MediatorResult를 반환하는지 테스트해봅니다.
@ExperimentalPagingApi
class PicsumRemoteMediator(
private val service: PicsumService,
private val database: PicsumDatabase
) : RemoteMediator<Int, Item>() {
...
}
- 테스트에 필요한 초기 작업을 해줍니다. 데이터베이스를 테스트하기 편하게 인메모리 방식으로, 실제 데이터베이스에 저장되지 않고, 인메모리에 저장되어 프로세스 종료 시에 사라지는 메모리 형태로 구현해 줍니다.
- Room 데이터 베이스르 만들기 위해서는 Context가 필요하기 때문에, AndroidJUnit4를 빌드하여 애플리케이션 콘텍스트에 접근할 수 있게 해야 합니다.
@ExperimentalCoroutinesApi
@ExperimentalPagingApi
@RunWith(AndroidJUnit4::class)
class PicsumRemoteMediatorTest {
private lateinit var db: PicsumDatabase
private val service: PicsumService = mockk()
private lateinit var picsumRemoteMediator: PicsumRemoteMediator
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, PicsumDatabase::class.java).build()
picsumRemoteMediator = PicsumRemoteMediator(
service,
db
)
}
@After
fun closeDb() {
db.close()
}
- 해당 테스트는 성공적인 응답을 반환하지만, 반환된 데이터가 비어있는 경우입니다.
- load() 메서드는 MediaResult.Success를 반환하고, endOfPaginationReached 속성은 true입니다.
@Test
fun refreshLoadReturnsSuccessResult() = runBlocking {
coEvery { service.getImages(0, 10) } returns listOf()
val pagingState = PagingState<Int, Item>(
listOf(),
null,
PagingConfig(10),
10
)
val result = picsumRemoteMediator.load(LoadType.REFRESH, pagingState)
Assert.assertTrue(result is RemoteMediator.MediatorResult.Success)
Assert.assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
}
Preference
Paging 구현 테스트
How to test Paging 3