๐ง Paging Library
ํ์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ๋คํธ์ํฌ(Remote)์ ๋ฐ์ดํฐ๋ฅผ ํ์ด์ง ๋จ์๋ก UI์ ์ฝ๊ฒ ํํํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๊ธฐ์กด์ ํ์ด์ง์ ๊ตฌํํ๊ธฐ ์ํด์๋ RecyclerView์ ๊ฐ์ ๋ฆฌ์คํธ UI๊ฐ ์๋จ ๋๋ ํ๋จ์ ๋๋ฌํ๋์ง ํ๋จํ๋ ์ฝ๋๋ฅผ ์์ฑํ๊ณ , ๋ค์ ํ์ด์ง๋ฅผ ๋ก๋(or Refresh)ํ๋ ์ฝ๋๋ฅผ ๋ ์์ฑํด์ผ๋ง ํ๋ค.
ํ์ด์ง์ด ํ์ํ ๋ชจ๋ ํ๋ฉด์ ๋์ผํ ์ฝ๋๋ฅผ ์์ฑํด์ผ๋ง ํ๊ณ ๋คํธ์ํฌ ์ค๋ฅ, ์คํฌ๋กค ๊ฐ์ง ์ด์(?)๊ณผ ๊ฐ์ ์์ธ ์ฒ๋ฆฌ ์ฝ๋๋ ์๋นํ๋ค.
์ ๋ฌธ์ ์ ๋ค์ ํฌํจํ ์ฌ๋ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ์ฌ Jetpack Paging Library๊ฐ ์ถ์๋์๋ค.
๊ทธ๋ฆฌ๊ณ ์ฌํด(2021๋ ) 5์ Paging 3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ Stable ๋ฒ์ ์ด ๋์๋ค.
๐ Paging 2 vs Paging 3
Paging 3 ์ ์ ์ถ์๋ก Paging 2.x.x ๊ธฐ์กด API ๋๋ถ๋ถ์ ์ง์ ์ค๋จํ ์ํ์ด๋ค.
๋ฌด์์ด ๋ฌ๋ผ์ก๋์ง ๊ฐ๋จํ๊ฒ๋ง ์ดํด๋ณด์.
- Coroutine, Flow, LiveData, RxJava๋ฅผ ์ํ ์ต๊ณ ์์ค์ ์ง์์ ์ ๊ณตํ๋ค.
- ์ฌ์๋(Retry)์ ์๋ก๊ณ ์นจ(Refresh) ๊ธฐ๋ฅ์ ํฌํจํ์ฌ ๋ฐ์ํ UI ๋์์ธ์ ์ํด LoadState์ Error Signal์ด ๋ด์ฅ๋์๋ค.
- Paging 2์ DataSource ์๋ธ ํด๋์ค๋ฅผ ๋ชจ๋ ํตํฉํ์ฌ ์ฌํํ ๋ฐ์ดํฐ์์ค ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค.
- ์ทจ์ ๊ธฐ๋ฅ์ด ๋ด์ฅ๋์๋ค.
- List Separator, Loading Header, Footer๊ฐ ๋ด์ฅ๋์๋ค.
- ๋ฐ์ดํฐ ์บ์ฑ์ด ๊ฐ๋ฅํ๋ค.
๐ฟ Components
Paging 3๋ ํ๋ก์ ํธ์ ์ ์ ํ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๋ฅผ ์๊ตฌํ๋ฉฐ ์๋๋ก์ด๋ ๊ถ์ฅ ์ํคํ ์ฒ์ ํตํฉ๋๋๋ก ๋ง๋ค์ด์ก๋ค.
Repository, ViewModel, UI 3๊ฐ์ง ๋ ์ด์ด์์ ์๋ํ๋ค.
๐ Repository Layer
๐ PagingData
ํ์ด์ง๋ ๋ฐ์ดํฐ์ Container ์ญํ ์ ํ๋ค. ๋ฐ์ดํฐ๊ฐ ์๋ก๊ณ ์นจ๋ ๋๋ง๋ค ์ด์ ์์ํ๋ PagingData๊ฐ ๋ณ๋๋ก ์์ฑ๋๋ค.
๐ PagingSource
๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ๋คํธ์ํฌ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ๊ฒ์ ๋ด๋นํ๋ ์ถ์ ํด๋์ค์ด๋ค.
๋ฐ์ดํฐ ์์ค๋ฅผ ์ ์ํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ์ ์ ์ํ๋ค.
๐ RemoteMediator
๋คํธ์ํฌ(Remote)์์ ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ฅผ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์บ์(Cache)ํ์ฌ ๋ถ๋ฌ์ค๋ ๊ฒ์ ๋ด๋นํ๋ค.
์คํ๋ผ์ธ ์ํ์์๋ ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ด์ผ๋ก ์ ์ ๊ฒฝํ์ ํฅ์์์ผ์ค ์ ์๋ค.
(ํ์ฌ Experimental ์ํ๋ก ํฅํ ๋ณ๊ฒฝ๋ ์ ์๋ค.)
๐ ViewModel Layer
๐ Pager
Repository Layer์์ ๊ตฌํ๋ PagingSource๊ณผ ํจ๊ป PagingData ์ธ์คํด์ค๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐ์ํ ์คํธ๋ฆผ์ ์์ฑํ๋ค.
PagingSource์์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๋ฐฉ๋ฒ, ์ต์ ์ ์ ์ํ PagingConfig ํด๋์ค์ ํจ๊ป ์ฌ์ฉ๋๋ค.
๐ UI Layer
๐ PagingDataAdapter
PagingData๋ฅผ RecyclerView์ ๋ฐ์ธ๋ฉํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค.
๋ฐ์ดํฐ๋ฅผ ์ด๋ ์์ ์์ ๋ ๋ฐ์์ฌ ๊ฒ์ธ๊ฐ ๋ฑ UI์ ๊ด๋ จ๋ ๋๋ถ๋ถ์ ์ผ์ ์ฑ ์์ง๋ค.
๐จ Implementation
๐ PagingSource ์ ์ํ๊ธฐ
PagingSource๋ฅผ ๋ง๋ค๊ธฐ ์ํด์๋ Paging Key์ธ ๋ฐ์ดํฐ ๋ก๋๋ฅผ ์ํ ์๋ณ์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ํด์ผํ๋ค.
ํ์ด์ง ๋ฒํธ ๋๋ offset๊ณผ ๊ฐ์ ์๋ณ์(key)๋ฅผ Retrofit(network) ๋๋ Room์ ์ ๋ฌํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๊ฒ์ด๋ค.
class TodoPagingSource(
private val dao: TodoDao
): PagingSource<Int, Todo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Todo> {
val page = params.key ?: 1
return try {
val items = dao.getTodoContentsByPaging(page, params.loadSize)
LoadResult.Page(
data = items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (items.isEmpty()) null else page + (params.loadSize / 10)
)
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Todo>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
- load(params: LoadParams<Key>)
- load ํจ์๋ ์ฌ์ฉ์๊ฐ ์คํฌ๋กค ํ ๋๋ง๋ค ๋ฐ์ดํฐ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ๊ฐ์ ธ์จ๋ค.
- LoadParams ๊ฐ์ฒด๋ ๋ก๋ ์์
๊ณผ ๊ด๋ จ๋ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
params.key์ ํ์ฌ ํ์ด์ง ์ธ๋ฑ์ค๋ฅผ ๊ด๋ฆฌํ๋ค. ์ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋์๋ null์ด ๋ฐํ๋๋ค.
params.loadSize๋ ๊ฐ์ ธ์ฌ ๋ฐ์ดํฐ์ ๊ฐฏ์๋ฅผ ๊ด๋ฆฌํ๋ค. - load ํจ์๋ LoadResult๋ฅผ ๋ฐํํ๋ค.
- LoadResult.Page : ๋ก๋์ ์ฑ๊ณตํ ๊ฒฝ์ฐ, ๋ฐ์ดํฐ์ ์ด์ ๋ค์ ํ์ด์ง Key๊ฐ ํฌํจ๋๋ค.
- LoadResult.Error : ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
- getRefreshKey()
- ์ค์์ดํ Refresh๋ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ ๋ฑ์ผ๋ก ํ์ฌ ๋ชฉ๋ก์ ๋์ฒดํ ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋ ์ฌ์ฉ๋๋ค.
PagingData๋ Component์์ ์ค๋ช ํ ๊ฒ์ฒ๋ผ ์๋ก๊ณ ์นจ ๋ ๋๋ง๋ค ์์ํ๋ PagingData๋ฅผ ์์ฑํด์ผํ๋ค.
์ฆ, ์์ ์ด ๋ถ๊ฐ๋ฅํ๊ณ ์๋ก์ด ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด์ผํ๋ค. - ๊ฐ์ฅ ์ต๊ทผ์ ์ ๊ทผํ ์ธ๋ฑ์ค์ธ anchorPosition์ผ๋ก ์ฃผ๋ณ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ก๋ํ๋ค.
- ์ค์์ดํ Refresh๋ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ ๋ฑ์ผ๋ก ํ์ฌ ๋ชฉ๋ก์ ๋์ฒดํ ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋ ์ฌ์ฉ๋๋ค.
๐ PagingData ๋น๋ ๋ฐ ๊ตฌ์ฑ
PagingData๋ฅผ ๊ตฌ์ฑํ๊ธฐ ์ํด์๋ ์ด๋ฅผ ๋ค๋ฅธ ์ฑ ๋ ์ด์ด์ ์ ๋ฌํ๊ธฐ ์ํ API๋ฅผ ๊ฒฐ์ ํด์ผํ๋ค.
์ฐ๋ฆฌ ํฌ์คํ ์์๋ Flow ํ์ ์ ์ฌ์ฉํ๊ฒ ๋ค. Flow ์ธ์๋ LiveData, RxJava(Flowable, Observable)์ ๋ชจ๋ ์ง์ํ๋ค.
class TodoRepository(private val todoDao: TodoDao) {
//..
fun getTodoContentItemsByPaging(): Flow<PagingData<Todo>> {
return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { TodoPagingSource(todoDao) }
).flow
}
}
- PagingConfig
- pageSize(ํ์ ๋งค๊ฐ๋ณ์) : ๊ฐ ํ์ด์ง์ ๋ก๋ํ ๋ฐ์ดํฐ ์๋ฅผ ๊ฐ๋ฆฌํจ๋ค.
- enablePlaceholders : ํ๋ ์ด์ค ํ๋ ์ฌ์ฉ ์ฌ๋ถ
- maxSize : ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ํ์ด์ง๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ๋ค. ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น ๋ฑ์ ํด๊ฒฐํ๊ธฐ ์ํด์ ์ค์ ํ ์ ์๋ค.
(์ถฉ๋ถํ ํฐ ์๋ก ์ง์ ํด์ผ๋ง ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ๋คํธ์ํฌ Request๊ฐ...)
- pagingSourceFactory
- PagingSource ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ค.
๐ PagingData ์์ฒญ ๋ฐ ์บ์ in ViewModel
class TodoViewModel(private val repository: TodoRepository): ViewModel() {
//..
fun getContent(): Flow<PagingData<Todo>> {
return repository.getTodoContentItemsByPaging()
.cachedIn(viewModelScope)
}
}
- cachedIn(CoroutineScope)
- CoroutineScope์์ ๋ฐ์ดํฐ๋ฅผ ์บ์ํ ์ ์๋ ๋ฉ์๋
- ViewModel์ด๊ธฐ ๋๋ฌธ์ lifecycle-viewmodel-ktx์ viewModelScope๋ฅผ ์ฌ์ฉํ๋ค.
๐ PagingData(RecyclerView)Adapter ์ ์ํ๊ธฐ
class TodoAdapter:
PagingDataAdapter<Todo, TodoAdapter.TodoViewHolder>(TODO_DIFF) {
//..
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
}
- getItem() ๋ฉ์๋๋ฅผ ํตํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
- placeholder์ผ ๊ฒฝ์ฐ null ๋ฐ์ดํฐ๊ฐ ๋ฐํ๋๋ค.
- DiffUtil์ ์ฌ์ฉํ๋ค.
(ํด๋น ํฌ์คํ ์์๋ DiffUtil์ ๋ค๋ฃจ์ง ์๋๋ค. ๊ฒ์ ๋๋ ํฌ์คํ ํ๋จ์ ๊นํ๋ธ ์ ์ฒด ์ฝ๋์์ ํ์ธํ์.)
๐ ๋ฐ์ดํฐ ์ฐ๊ฒฐํ๊ธฐ
Activity ๋๋ Fragment์์ ViewModel์์ ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ PagingDataAdapter๋ก ์ ๋ฌํ์.
class TodoFragment : Fragment() {
//..
private fun initTodoJob() {
//..
lifecycleScope.launch {
viewModel.getContent().collectLatest {
adapter.submitData(it)
}
}
}
private fun initAdapter() {
adapter = TodoAdapter()
binding.rvTodoList.adapter = adapter
}
}
๐ Reference
์๋๋ก์ด๋ ๊ณต์ ๋ฌธ์ - Paging 3
RemoteMediator, LoadState, Header, Footer ๋ฑ์ ๋ค์ ํฌ์คํ ์์ ๋ค๋ฃจ๋๋ก ํ๊ฒ ๋ค.
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ๊ฐ๋ฅํ๋ค. ๊นํ๋ธ ๋ฐ๋ก๊ฐ๊ธฐ
๋๊ธ