- Paging3
๋ก์ปฌ ์ ์ฅ์์์๋ ๋คํธ์ํฌ๋ฅผ ํตํด ๋๊ท๋ชจ ๋ฐ์ดํฐ ์ธํธ์ ๋ฐ์ดํฐ ํ์ด์ง๋ฅผ ๋ก๋ํ๊ณ ํ์ํ์ฌ ๋คํธ์ํฌ ๋์ญํญ๊ณผ ์์คํ
๋ฆฌ์์ค๋ฅผ ๋ชจ๋ ๋ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
์ฝ๊ฒ ๋งํ๋ฉด ์๋๋ก์ด๋์์ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ๋คํธ์ํฌ(Remote)์ ๋ฐ์ดํฐ๋ฅผ ํ์ด์ง ๋จ์๋ก UI์ ์ฝ๊ฒ ํํํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
๊ธฐ์กด์ ํ์ด์ง์ ๊ตฌํํ๋ ๋ฐฉ์์ RecyclerView์ ๊ฐ์ ๋ฆฌ์คํธ UI๊ฐ ์๋จ ๋๋ ํ๋จ์ ๋๋ฌํ๋์ง ํ๋จํ๋ ์ฝ๋๋ฅผ ์์ฑํ ๋ค,
ํ๋จ์ ๋๋ฌํ์์๋ ๋ค์ ํ์ด์ง๋ฅผ ๋ก๋(or Refresh)ํ๋ ์ฝ๋๋ฅผ ์คํํ๋ ๋ฐฉ์์ด ์ฃผ๋ฅผ ์ด๋ฃจ์๋ค.
ํ์ด์ง์ด ํ์ํ ๋ชจ๋ ํ๋ฉด์ ๋์ผํ ์ฝ๋๋ฅผ ์์ฑํด์ผ๋ง ํ๊ณ ๋คํธ์ํฌ ์ค๋ฅ, ์คํฌ๋กค ์ค๋ฅ๋ฑ์ ์์ธ ์ฒ๋ฆฌ ์ฝ๋๊ฐ ๋ง์ด ํ์ํ์๋ค.
Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์์ ๊ฐ์ ๊ณผ์ ์์ด ์ฝ๊ฒ ํ์ด์ง์ ๊ตฌํํ ์ ์๋ค.
Paging 3์ ์ด์ Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ ๊ณผ ํฌ๊ฒ ๋ฌ๋ผ์ก๋ค.
- ํ์ด์ง๋ ๋ฐ์ดํฐ์ ๋ฉ๋ชจ๋ฆฌ ๋ด ์บ์ฑ ์ง์
- ์ฑ์ด ํ์ด์ง ๋ฐ์ดํฐ๋ก ์์ ํ๋ ๋์ ์์คํ ๋ฆฌ์์ค๋ฅผ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ ์์
- ์์ฒญ ์ค๋ณต ์ ๊ฑฐ ๊ธฐ๋ฅ์ด ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต
- ์ฑ์์ ๋คํธ์ํฌ ๋์ญํญ๊ณผ ์์คํ ๋ฆฌ์์ค๋ฅผ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ ์ ์์
- ์ฌ์ฉ์๊ฐ ๋ก๋๋ ๋ฐ์ดํฐ์ ๋๊น์ง ์คํฌ๋กคํ ๋ ๊ตฌ์ฑ ๊ฐ๋ฅํ RecyclerView ์ด๋ํฐ๊ฐ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฒญ
- Kotlin Coroutine ๋ฐ Flow๋ฟ๋ง ์๋๋ผ LiveData ๋ฐ RxJava ์ต๊ณ ์์ค ์ง์
- ์๋ก๊ณ ์นจ ๋ฐ ์ฌ์๋ ๊ธฐ๋ฅ์ ํฌํจํ์ฌ ์ค๋ฅ ์ฒ๋ฆฌ ๊ธฐ๋ณธ์ผ๋ก ์ง์
- ๊ธฐ์กด Paging2์ DataSource๋ค์ ํตํฉ, ์ฌํํ DataSource ์ธํฐํ์ด์ค ์ ๊ณต
DatPageKeyedDataSource
,PositionalDataSource
,ItemKeyedDataSource
ํตํฉ
- Header, Footer ์ง์
PagingData
: ํ์ด์ง๋ก ๋๋ ๋ฐ์ดํฐ์ ์ปจํ ์ด๋. ๋ฐ์ดํฐ๋ฅผ ์๋ก๊ณ ์นจํ ๋๋ง๋ค ์์ํ๋ PagingData๊ฐ ๋ณ๋๋ก ์์ฑ๋๋คPagingSource
:PagingSource
๋ ๋ฐ์ดํฐ์ ์ค๋ ์ท์ PagingData์ ์คํธ๋ฆผ์ผ๋ก ๋ก๋ํ๊ธฐ ์ํ ๊ธฐ๋ณธ ํด๋์คPager.flow
: ๊ตฌํ๋PagingSource
์ ๊ตฌ์ฑ ๋ฐฉ๋ฒ์ ์ ์ํ๋ ํจ์์ PagingConfig๋ฅผ ๊ธฐ๋ฐ์ผ๋กFlow<PagingData>
๋ฅผ ๋น๋PagingDataAdapter
:RecyclerView
์PagingData
๋ฅผ ํ์ํ๋RecyclerView.Adapter
.PagingDataAdapter
๋Kotlin Flow
,LiveData
,RxJava
Flowable
๋๋RxJava Observable
์ ์ฐ๊ฒฐํ ์ ์๋ค.PagingDataAdapter
๋ ํ์ด์ง๊ฐ ๋ก๋๋ ๋ ๋ด๋ถPagingData
๋ก๋ฉ ์ด๋ฒคํธ๋ฅผ ์์ ๋๊ธฐํ๊ณ ์ ๋ฐ์ดํธ๋ ์ฝํ ์ธ ๊ฐ ์๋ก์ดPagingData
๊ฐ์ฒด ํํ๋ก ์์ ๋ ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ DiffUtil๋ฅผ ์ฌ์ฉํ์ฌ ์ธ๋ถํ๋ ์ ๋ฐ์ดํธ๋ฅผ ๊ณ์ฐํ๋คRemoteMediator
: ๋คํธ์ํฌ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ด์ง๋ก ๋๋๊ธฐ๋ฅผ ๊ตฌํํ๋ ๋ฐ ์ ์ฉ
PagingSource๋ฅผ ์ ์ํ๋ ค๋ฉด ์๋์ ํญ๋ชฉ์ ์ ์ํด์ผํ๋ค.
- ํ์ด์ง ํค์ ์ ํ: ํ์ฌ ๋ก๋ํ ๋ฐ์ดํฐ์ ํ์ด์ง ์ ๋ณด ๋ฐ์ดํฐ์ ํ์ , ์์์์ ๊ฒ์ API์์ ํ์ด์ง์ 1์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ์์ธ ๋ฒํธ๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์ ํ์ Int
- ๋ก๋๋ ๋ฐ์ดํฐ์ ์ ํ: ์๋ต ๋ชจ๋ธ ํ์
SearchDocument
- ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์์น: Retrofit์์ ๊ฐ์ ธ์ค๋ฏ๋ก
SearchApi
class SearchPagingSource(
private val query: String,
private val api: SearchApi
) : PagingSource<Int, SearchDocument>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SearchDocument> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, SearchDocument>): Int? {
TODO("Not yet implemented")
}
}
PagingSource
์์๋ ๋ ๊ฐ์ง ํจ์๋ฅผ ๊ตฌํํด์ผํ๋ค.
public abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
public abstract fun getRefreshKey(state: PagingState<Key, Value>): Key?
load
ํจ์๋ LoadResult
๋ฅผ ๋ฐํํด์ผํ๋ค.
LoadResult.Page
: ๋ก๋์ ์ฑ๊ณตํ ๊ฒฝ์ฐLoadResult.Error
: ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
LoadResult.Page
๋ฅผ ๊ตฌ์ฑํ ๋ ์์ํ๋ ๋ฐฉํฅ์ผ๋ก ๋ชฉ๋ก์ ๋ก๋ํ ์ ์๋ ๊ฒฝ์ฐ nextKey
๋๋ prevKey
์ null์ ์ ๋ฌํ๋ค.
์๋ฅผ ๋ค์ด ๋คํธ์ํฌ ์๋ต์ ์ฑ๊ณตํ์ง๋ง ๋ชฉ๋ก์ด ๋น์ด ์๋ ๊ฒฝ์ฐ์๋ ๋ก๋ํ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ nextKey๊ฐ null์ผ ์ ์๋ค.
์๋ก๊ณ ์นจํค๋ PagingSource.load()
์ ํ์ ์๋ก๊ณ ์นจ ํธ์ถ์ ์ฌ์ฉ ๋๋ค.
์ฒซ ๋ฒ์งธ ํธ์ถ์ Pager์ ์ํด ์ ๊ณต๋๋ initialKey๋ฅผ ์ฌ์ฉํ๋ค.
์๋ก๊ณ ์นจ์ ์ค์์ดํํ์ฌ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ, ๊ตฌ์ฑ ๋ณ๊ฒฝ, ํ๋ก์ธ์ค ์ค๋จ ๋ฑ์ผ๋ก ์ธํด ๋ฌดํจํ๋์ด
Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ฌ ๋ชฉ๋ก์ ๋์ฒดํ ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ค๊ณ ํ ๋๋ง๋ค ๋ฐ์ํ๋ค.
์ผ๋ฐ์ ์ผ๋ก ํ์ ์๋ก๊ณ ์นจ ํธ์ถ์ ๊ฐ์ฅ ์ต๊ทผ์ ์ก์ธ์คํ ์ธ๋ฑ์ค๋ฅผ ๋ํ๋ด๋ PagingState.anchorPosition
์ฃผ๋ณ ๋ฐ์ดํฐ์ ๋ก๋๋ฅผ ๋ค์ ์์ํ๋ค.
PagingData
๋ PagingData
๋ฅผ ๋ค๋ฅธ ๋ ์ด์ด๋ก ์ ๋ฌํ API๋ฅผ ๊ฒฐ์ ํด์ผํ๋ค.
- Kotlin Flow:
Pager.flow
์ฌ์ฉ - LiveData:
Pager.liveData
์ฌ์ฉ - RxJava Flowable:
Pager.flowable
์ฌ์ฉ - RxJava Observable:
Pager.observable
์ฌ์ฉ
๋ํ PageData
๋ ์๋์ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํด์ผ ํ๋ค.
- PagingConfig
- ๋ก๋ ๋๊ธฐ ์๊ฐ, ์ด๊ธฐ ๋ก๋์ ํฌ๊ธฐ ์์ฒญ ๋ฑ PagingSource์์ ์ฝํ ์ธ ๋ฅผ ๋ก๋ํ๋ ๋ฐฉ๋ฒ์ ๊ดํ ์ต์ ์ค์
- ์ ์ํด์ผ ํ๋ ์ ์ผํ ํ์ ๋งค๊ฐ๋ณ์๋ ๊ฐ ํ์ด์ง์ ๋ก๋ํด์ผ ํ๋ ํญ๋ชฉ ์๋ฅผ ๊ฐ๋ฆฌํค๋ ํ์ด์ง ํฌ๊ธฐ์ด๋ค
- ๊ธฐ๋ณธ์ ์ผ๋ก Paging์ ๋ก๋ํ๋ ๋ชจ๋ ํ์ด์ง๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ๋ค. ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ญ๋นํ์ง ์์ผ๋ ค๋ฉด
PagingConfig
์์maxSize
๋งค๊ฐ๋ณ์๋ฅผ ์ค์ ํด์ผํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก Paging์ ๋ก๋๋์ง ์์ ํญ๋ชฉ์ ์ง๊ณํ ์ ์๊ณenablePlaceholders
๊ตฌ์ฑ ํ๋๊ทธ๊ฐ true์ธ ๊ฒฝ์ฐ ์์ง ๋ก๋๋์ง ์์ ์ฝํ ์ธ ์ ์๋ฆฌํ์์๋ก null ํญ๋ชฉ์ ๋ฐํํ๋ค PagingConfig.pageSize
๋ ์ฌ๋ฌ ํ๋ฉด์ ํญ๋ชฉ์ด ํฌํจ๋ ๋งํผ ์ถฉ๋ถํ ์ปค์ผํ๋ค. ํ์ด์ง๊ฐ ๋๋ฌด ์์ผ๋ฉด ํ์ด์ง์ ์ฝํ ์ธ ๊ฐ ์ ์ฒด ํ๋ฉด์ ๊ฐ๋ฆฌ์ง ์๊ธฐ ๋๋ฌธ์ ๋ชฉ๋ก์ด ๊น๋ฐ์ผ ์ ์๋ค. ํ์ด์ง ํฌ๊ธฐ๊ฐ ํด์๋ก ๋ก๋ ํจ์จ์ด ์ข์ง๋ง ๋ชฉ๋ก์ด ์ ๋ฐ์ดํธ๋ ๋ ์ง์ฐ ์๊ฐ์ด ๋์ด๋ ์ ์๋ค- ๊ธฐ๋ณธ์ ์ผ๋ก
PagingConfig.maxSize
๋ ๋ฌด์ ํ์ด๋ฏ๋ก ํ์ด์ง๊ฐ ์ญ์ ๋์ง ์๋๋ค. ํ์ด์ง๋ฅผ ์ญ์ ํ๋ ค๋ฉด ์ฌ์ฉ์๊ฐ ์คํฌ๋กค ๋ฐฉํฅ์ ๋ณ๊ฒฝํ ๋ ๋คํธ์ํฌ ์์ฒญ์ด ๋๋ฌด ๋ง์ด ๋ฐ์ํ์ง ์๋๋กmaxSize
๋ฅผ ์ถฉ๋ถํ ํฐ ์๋ก ์ ์งํด์ผ ํ๋ค. ์ต์๊ฐ์pageSize + prefetchDistance * 2
์ด๋ค
- PagingSource๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ ์ํ๋ ํจ์
PagingConfig
๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Flow<PagingData<T>>
๋ฅผ ์ ๋ฌ
fun getSearchResultStream(query: String): Flow<PagingData<SearchDocument>> {
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false,
),
pagingSourceFactory = { SearchPagingSource(query, api) }
).flow
}
PagingData
๋ฅผ RecyclerView
์ ๋ฐ์ธ๋ฉํ๋ ค๋ฉด PagingDataAdapter
๋ฅผ ์ฌ์ฉํ๋ฉด๋๋ค.
PagingData
์ฝํ
์ธ ๊ฐ ๋ก๋๋ ๋๋ง๋ค PagingDataAdapter
์์ ์๋ฆผ์ ๋ฐ์ ๋ค์ RecyclerView
์ ์
๋ฐ์ดํธํ๋ผ๋ ์ ํธ๋ฅผ ๋ณด๋ธ๋ค.
PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder>
T๋ PagingData
์ ํ์
์ด๋ค.
class SearchPagingAdapter : PagingDataAdapter<SearchUiItem, RecyclerView.ViewHolder>(SearchDiffUtil()) {
// body is unchanged
}
LoadStateAdapter
๋ ๋ก๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์๋์ผ๋ก ์๋ฆผ์ ๋ฐ๋๋ค.
์ํ๋ LoadState
๊ฐ์ผ๋ก onCreateViewHolder
, onBindViewHolder
ํจ์๋ก ๋์ด์ค๊ฒ ๋๋ค.
class SearchPagingLoadStateAdapter(private val retry: () -> Unit) : LoadStateAdapter<SearchLoadStateViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): SearchLoadStateViewHolder {
return SearchLoadStateViewHolder.create(parent, retry)
}
override fun onBindViewHolder(holder: SearchLoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
}
LoadState
๊ฐ LoadState.Loading
์ผ๋๋ ProgressBarํ์, LoadState.Error
์ผ๋๋ Retry๋ทฐ๋ฅผ ํ์ํด์ฃผ๋ฉด ๋๋ค.
์ฌ์๋ ๋งค์ปค๋์ฆ์ PagingDataAdapter
์ retryํจ์๋ฅผ ์ฌ์ฉํ๋ค.
PagingDataAdapter
์ ํค๋์ ํธํฐ๋ ์๋์ฒ๋ผ ๋ฑ๋กํ๋ค.
private fun initAdapter() {
binding.list.adapter = adapter.withLoadStateHeaderAndFooter(
header = SearchPagingLoadStateAdapter { adapter.retry() },
footer = SearchPagingLoadStateAdapter { adapter.retry() }
)
}
๊ตฌ๋ถ์๋ฅผ ์ถ๊ฐํ๋ฉด ๋ชฉ๋ก์ ๊ฐ๋ ์ฑ์ ๊ฐ์ ํ ์๋ ์๋ค. ์๋ฅผ๋ค์ด ๋ ์ง๋ณ๋ก ์ ๋ ฌํ๋ค๊ณ ํ ๋ ์ ๋ณ๋ก ๊ตฌ๋ถํ๋ ๊ตฌ๋ถ์๋ฅผ ์ถ๊ฐํ ์ ์๊ฒ ๋ค.
PagingData์ ๋ชจ๋ธ ๋ณ๊ฒฝ
sealed class SearchUiItem {
data class DocumentItem(val document: Document) : SearchUiItem()
data class SeparatorItem(val description: String) : SearchUiItem()
}
private fun getPager(requestParam: SearchPagingParam): LiveData<PagingData<SearchUiItem>> {
return getSearchPagingData(requestParam)
.map { pagingData -> pagingData.map { document -> SearchUiItem.DocumentItem(document) } }
.map { pagingData ->
when (sort) {
SortType.TITLE -> insertTitleSeparator(pagingData)
else -> insertDateSeparator(pagingData)
}
}
.cachedIn(viewModelScope)
.asLiveData()
}
private fun insertTitleSeparator(it: PagingData<SearchUiItem.DocumentItem>): PagingData<SearchUiItem> {
return it.insertSeparators { before, after ->
if (after == null) return@insertSeparators null
if (before == null) return@insertSeparators SearchUiItem.SeparatorItem("${after.document.title.first()}")
val beforeFirstWord = before.document.title.first()
val afterFirstWord = after.document.title.first()
return@insertSeparators when (beforeFirstWord != afterFirstWord) {
true -> SearchUiItem.SeparatorItem("${after.document.title.first()}")
else -> null
}
}
}
private fun insertDateSeparator(it: PagingData<SearchUiItem.DocumentItem>): PagingData<SearchUiItem> {
return it.insertSeparators { before, after ->
if (after == null) return@insertSeparators null
if (before == null) return@insertSeparators SearchUiItem.SeparatorItem(
dateHelper.convert(after.document.date)
)
val beforeDate = before.document.date
val afterDate = after.document.date
val beforeDateString = dateHelper.convert(beforeDate, R.string.date_month)
val afterDateString = dateHelper.convert(afterDate)
return@insertSeparators when (beforeDateString != afterDateString) {
true -> SearchUiItem.SeparatorItem(description = afterDateString)
else -> null
}
}
}
๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ฌ ์ฑ์ ์คํ๋ผ์ธ ์ง์์ ์ถ๊ฐํ ์ ์๋ค.
์ด๋ ๊ฒ ํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ฑ์ ์ ๋ณด ์์ค๊ฐ ๋๊ณ ํญ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ฐ์ดํฐ๊ฐ ๋ก๋ ๋๋ค.
๋ ์ด์ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ง๋ค ๋คํธ์ํฌ์ ๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ ๋ณด ์์ค์ด๋ฏ๋ก ๋ ๋ง์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ฉด UI๊ฐ ์๋์ผ๋ก ์
๋ฐ์ดํธ ๋๋ค.
๋ชฉ๋ก์ ๋ก๊ฑธ DB์ ์ ์ฅํ๋ฏ๋ก ์๋์๊ฐ์ด Entity(ํ ์ด๋ธ)๋ฅผ ์์ฑํ๋ค.
@Entity(tableName = "document_table")
internal data class DocumentTable(
@field:SerializedName("type")
val type: DocumentType,
@PrimaryKey
@field:SerializedName("url")
val url: String,
@field:SerializedName("thumbnail")
val thumbnail: String,
@field:SerializedName("title")
val title: String,
@field:SerializedName("content")
val content: String,
@field:SerializedName("date")
val date: Date?,
)
RemoteMediator
์
PagingState
์์ ๋ง์ง๋ง์ผ๋ก ๋ก๋๋ ํญ๋ชฉ์ ๊ฐ์ ธ์ค๋ฉด ํญ๋ชฉ์ด ์ํ ํ์ด์ง์ ์์ธ์ ์ ์ ์์ผ๋ฏ๋ก,
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด remote_key
๋ผ๊ณ ํ๋ ๋ค์ ๋ฐ ์ด์ ํ์ด์ง ํค๋ฅผ ์ ์ฅํ๋ ๋ค๋ฅธ Entity(ํ
์ด๋ธ)์ ์ถ๊ฐ ํ๋ค.
@Entity(tableName = "remote_key")
data class RemoteKeyTable(
@PrimaryKey
@field:SerializedName("position")
val position: Int? = -1,
@field:SerializedName("prev_key")
val prevKey: Int?,
@field:SerializedName("next_key")
val nextKey: Int?
)
Entity(ํ ์ด๋ธ)์ ์ ๊ทผํ๊ธฐ์ํ ์ธํฐํ์ด์ค ์ถ๊ฐ, Title ๋๋ Date๋ก ์ ๋ ฌํ์ฌ ํ ์ด๋ธ์์ ๊ฐ์ ธ์ค๋๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ค.
@Dao
internal interface DocumentDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDocuments(documents: List<DocumentTable>)
@Query(
"SELECT * FROM document_table " +
"WHERE title Like '%'||:query||'%' or content Like '%'||:query||'%' " +
"ORDER BY date DESC"
)
fun getDocumentByDate(query: String): PagingSource<Int, DocumentTable>
@Query(
"SELECT * FROM document_table " +
"WHERE title Like '%'||:query||'%' or content Like '%'||:query||'%' " +
"ORDER BY title ASC"
)
fun getDocumentByTitle(query: String): PagingSource<Int, DocumentTable>
@Query("DELETE from document_table")
suspend fun clearAllDocuments()
}
๋ค์ ๋ฐ ์ด์ ํ์ด์ง ํค๋ฅผ ์ ์ฅํ๋ ๋ค๋ฅธ Entity(ํ ์ด๋ธ)๋ ์ ๊ทผํ ์ ์๋ ์ธํฐํ์ด์ค๋ฅผ ์ ์.
@Dao
interface RemoteKeyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertKey(remoteKey: RemoteKeyTable)
@Query("SELECT * FROM remote_key")
suspend fun getRemoteKeyTable(): Array<RemoteKeyTable>?
@Query("DELETE FROM remote_key")
suspend fun clearRemoteKeys()
}
@Database(
entities = [DocumentTable::class, RemoteKeyTable::class],
version = 1,
exportSchema = false
)
@TypeConverters(DateConverter::class)
internal abstract class AppDatabase : RoomDatabase() {
abstract fun documentDao(): DocumentDao
abstract fun remoteKeyDao(): RemoteKeyDao
companion object {
private const val NAME = "app_database"
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context,
AppDatabase::class.java,
NAME
).build()
}
}
}
}
RemoteMediator
๋ load
ํจ์๋ฅผ ํ์๋ก ๊ตฌํํด์ผ ํ๋ค.
public abstract suspend fun load(
loadType: LoadType,
state: PagingState<Key, Value>
): MediatorResult
load
ํจ์๋ LoadType
๊ณผ, PagingState
๋ฅผ ์ ๋ฌํ๋ฉฐ,
๋ฐํ๊ฐ์ผ๋ก MediatorResult
๋ฅผ ๋ฐํํด์ผํ๋ค.
LoadType
LoadType.REFRESH
:PagingSource
๊ฐ ๋ฌดํจํ๋์๊ณ ,PagingData
๋ฅผ ์๋ก๊ณ ์นจํ ๋, ์ด๊ธฐํ ์ ํธLoadType.PREPEND
:PagingSource
์ ํ์ฌ ๋ฐ์ดํฐ์ ์ฒซ๋ฒ์งธ ํ์ด์ง์ ๋๋ฌํ์์๊ฒฝ์ฐLoadType.APPEND
:PagingSource
์ ํ์ฌ ๋ฐ์ดํฐ์ ๋ง์ง๋ง ํ์ด์ง์ ๋๋ฌํ์์๊ฒฝ์ฐ
PagingState : ์ด์ ์ ๋ก๋๋ ํ์ด์ง, ๋ชฉ๋ก์์ ๊ฐ์ฅ ์ต๊ทผ์ ์ก์ธ์คํ index, ํ์ด์ง ์คํธ๋ฆผ์ ์ด๊ธฐํํ ๋ ์ ์ํ PagingConfig
์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ค.
MediatorResult
MediatorResult.Success
: ๋คํธ์ํฌ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ ๊ฒฝ์ฐ, ์ฌ๊ธฐ์์ ๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ์ ์๋์ง ์ฌ๋ถ(endOfPaginationReached
๋ก)๋ฅผ ์ ๋ฌํ๋คMediatorResult.Error
: ๋คํธ์ํฌ์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ๋์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ๋ฐํ
load ํจ์์์ ์ ๊ณต๋๋ LoadType์ ํ์ฉํ์ฌ ๋ฐ์ดํฐ ์ถ๊ฐ๋ก๋ ์ฌ๋ถ์ ๋ํ ๋ก์ง์ ๊ตฌํํ ๋ค, ๊ฒฐ๊ณผ๋ฅผ loadํจ์์ ๋ฐํํ์ธ MediatorResult
์ endOfPaginationReached
ํ๋ผ๋ฏธํฐ์ ๋๊ฒจ์ฃผ์ด ๋ฐ์ดํฐ ๋ก๋๋ฅผ ๋๋ง์น ์ง ํ๋จํ๋ค.
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, DocumentTable>
): MediatorResult {
try {
val position: Int = when (loadType) {
LoadType.REFRESH -> STARTING_POSITION
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val key = database.remoteKeyDao().getRemoteKeyTable()?.lastOrNull()
val nextKey = key?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = key != null)
nextKey
}
}
database.withTransaction {
if (loadType == LoadType.REFRESH) {
database.documentDao().clearAllDocuments()
database.remoteKeyDao().clearRemoteKeys()
}
}
val documentList = fetchDocumentList(position)
database.withTransaction {
val prevKey = if (position == STARTING_POSITION) null else position - 1
val nextKey = if (documentList.hasMore) position + 1 else null
val keys = RemoteKeyTable(position = position, prevKey = prevKey, nextKey = nextKey)
database.documentDao().insertDocuments(documentList.documents.map { it.toTable() })
database.remoteKeyDao().insertKey(keys)
}
return MediatorResult.Success(endOfPaginationReached = !documentList.hasMore)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
Pager
์ remoteMediator์ pagingSourceFactory๋ฅผ ์ ๋ฌํ๋ค.
@ExperimentalPagingApi constructor(
config: PagingConfig,
initialKey: Key? = null,
remoteMediator: RemoteMediator<Key, Value>?,
pagingSourceFactory: () -> PagingSource<Key, Value>
) {
...
pagingSourceFactory๋ ์ ๋ ฌ ํ์ ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ ๋ฌ๋๋๋ก ๊ตฌํ.
@Singleton
internal class DocumentRepositoryImpl @Inject constructor(
private val searchRemoteMediatorFactory: SearchRemoteMediatorFactory,
private val documentDao: DocumentDao
) : DocumentRepository {
override fun fetchDocumentPagingData(param: SearchPagingParam): Flow<PagingData<Document>> {
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = SearchRemoteMediator.PER_PAGE_SIZE,
prefetchDistance = 3
),
remoteMediator = searchRemoteMediatorFactory.create(param)
) {
when (param.sortType) {
SortType.TITLE -> documentDao.getDocumentByTitle(param.query)
else -> documentDao.getDocumentByDate(param.query)
}
}.flow.map {
it.map { documentTable ->
documentTable.toEntity()
}
}
}
}
์์ฑํ PagingData๋ PagingDataAdapter๋ก ์ ๋ฌํ๋ค.