๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํด์ผ๋๋๋ฐ, ๊ธฐ์กด์ ๊ฒฝํํด๋ดค๋ ๋ฆฌ์ฌ์ดํด๋ฌ ๋ทฐ ์คํฌ๋กค ๊ฐ์ง๋ฐฉ๋ฒ ๋ง๊ณ ํ์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด๋ณด๊ธฐ๋ก ํ๋ค. ์ด๋ฒ์ ์ฌ์ฉํ ๊ฑด Paging3๋ค.
https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko
์ด๊ธฐ ์ธํ ๊ฐ์ ๊ฑด ๋ฌธ์๋ณด๋ฉด ์ ๋์์์ผ๋ ๋๋ ๋ด๊ฐ ์ ์ฉํด๋ณด๋ฉด์ ๋งํ๋ ๋ถ๋ถ ์์ฃผ๋ก ์ ๊ฒ ๋ค.\
# Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ
ํ์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ Android Jetpack์ ์ํด์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ฉฐ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์์ ๋ฉ์ด๋ฆฌ๋ก ๋๋ ์ ํจ์จ์ ์ผ๋ก ๋ก๋ํ๊ณ ํ์ํด์ค๋ค. ์คํฌ๋กค์ด๋ฒคํธ๋ฅผ ์์์ ๊ฐ์ ธ๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๋ก๋ํ๊ธฐ ๋๋ฌธ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ต์ ํ ๋์ด์๋ค๊ณ ๋ณผ ์ ์๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ฉด์ ๋ฌด์กฐ๊ฑด ๋ง๋๊ฒ ๋ ์ปดํฌ๋ํธ 5๊ฐ์ง๊ฐ ์๋ค.
- `PagingSource`: ๋ฐ์ดํฐ ์์ค๋ฅผ ์ ์ํ๋ ๊ณณ์ด๋ค. ํ์ด์ง ๋ณ๋ก api๋ db๋ฅผ ํธ์ถํ๋ ๊ฒ ์ฌ๊ธฐ์ ํด๋นํ๋ค.
- `PagingConfig`: ํ์ด์ง ๋์์ ๊ตฌ์ฑํ๋ค. ํ์ด์ง ํฌ๊ธฐ๋ ์ผ๋ง๋ก ํ ๊ฑด์ง ์ ํ๋ ๊ณณ์ด ์ฌ๊ธฐ๋ค.
- `Pager`: PagingSource์ PagingConfig๋ฅผ ์ฌ์ฉํ์ฌ PagingData ์คํธ๋ฆผ์ ์์ฑํด flow๋ก ์๋ ๊ณณ์ด๋ค.
- `PagingData`: ํ์ด์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ํ๋ด๋ ์ปจํ ์ด๋๋ค. PagingData๋ผ๊ณ ํด์ ๋จ์ผ๋ฐ์ดํฐ๋ก ์ฐฉ๊ฐํ๋ฉด ์๋๋ค.
- `PagingDataAdapter`: RecyclerView Adapter์ ๋ฌ์์ฃผ๋ ๊ฑด๋ฐ, DiffUtil์ด ์ฌ์ฉ๋๋ฏ๋ก ์ฌ์ค์ ListAdapter์ธ๋ฐ ํ์ด์ง์ด ๋ฌ๋ฆฐ๊ฑธ๋ก ์ดํดํ๋ฉด๋๋ค. paging item์ด nullable ํ ๊ฒ๊ณผ, submitList๊ฐ ์๋๋ผ submitData๋ก ๋ฉ์๋๊ฐ ๋ฐ๋ ๊ฒ ๋นผ๊ณ ๋ ๊ฑฐ์ ๋์ผํ๋ค.
์ฝ๋๋ก ๋ณด๊ธฐ์ ์ ์ํคํ ์ณ์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง๋ง ๋งํ๊ณ ๊ฐ๊ฒ ๋ค.
๊ณต์๋ฌธ์์ ์ฐ์ฌ์๋ ํ์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํคํ ์ณ ์ ์ฉ๋ฒ์ด๋ค. ๋๋ PagingSource๋ฅผ data layer์ ๋๊ณ , repository์์ Pager๋ฅผ ์ฒ๋ฆฌํ๊ฒ ํ๋ค. ๊ณต์๋ฌธ์์ ์กฐ๊ธ ๋ค๋ฅด์ง๋ง ํ์ฌ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๊ณ ์๋ ์ํคํ ์ณ๊ฐ `API -> DataSource -> Repository -> UseCase -> ViewModel`์ด๋ผ์ ๊ธฐ์กด ํจํด์ ์ต๋ํ ์งํค๊ธฐ ์ํด ์ ํํ ๋ฐฉ์์ด๋ค.
๋ด๊ฐ ์์ฑํ PagingSource ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
class FeedPagingSource(private val feedDataSource: FeedDataSource) :
PagingSource<Int, Feed>() {
override fun getRefreshKey(state: PagingState<Int, Feed>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Feed> {
val nextPage = params.key ?: 1
return when (val result = safeApiCall { feedDataSource.getFeed(nextPage, params.loadSize) }) {
is ApiResult.Success -> {
val response = result.data
Timber.tag("pager").d("${response.items.firstOrNull()?.feedId}")
LoadResult.Page(
data = response.items,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (response.items.isEmpty() || nextPage >= response.totalPages) null else nextPage + 1
)
}
is ApiResult.Error -> {
Timber.e(result.exception, "๋ค์ ํ์ด์ง ๋ชป ๋ถ๋ฌ์์ $nextPage")
LoadResult.Error(result.exception)
}
}
}
}
PagingSource๋ฅผ ํ์ฅํ๋ฉด, getRefreshKey์ load๋ ํ์๋ก overrideํด์ผ๋๋ค. load๋ ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๋ก์ง์ ๊ตฌํํ๋ ๋ถ๋ถ์ด๋ค. ํ์ด์ง ๋ฒํธ์ ๋ก๋ํ ์์ดํ ์๋ฅผ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค. getRefreshKey๋ ์๋ก๊ณ ์นจ ์ ์์ํ ํ์ด์ง ํค๋ฅผ ๊ฒฐ์ ํ๋ ๋ฉ์๋๋ค.
๋ ๋ถ๋ถ์ ๋ํด ์ข ์ค๋ช ํด๋ณด๊ฒ ๋ค. load๋จผ์ ๋ณด์.
`params.key`๋ก ๋ก๋ํ ํ์ด์ง ๋ฒํธ๋จผ์ ๋ฐ๋๋ค. ๋ฐ์ดํฐ ์์ค์์ ํธ์ถํ api ๊ฒฐ๊ณผ๊ฐ success๋ฉด `LoadResult.Page`๋ฅผ ๋ฐํํ๊ณ , Error๋ฉด `LoadResult.Error`๋ฅผ ๋ฐํํ๋ค. ์ฐ๋ฆฌ๊ฐ ํ์ํ ๊ฑด `LoadResult.Page`๋ค. List๋ฅผ data๋ก ๋ฐ๊ณ , ์ด์ ํ์ด์ง ํค์ ๋ค์ ํ์ด์ง ํค๋ฅผ ๋ฐ๋๋ค. ๋๋ ํ์ฌ ํ์ด์ง๊ฐ ์ ์ฒด ํ์ด์ง ๋ฒํธ๋ ๊ฐ์ ๋๋ฅผ ํ์ด์ง ๋์ด๋ผ๊ณ ๋ช ์ํด๋๋ค.
getRefreshKey๋ ์ด๋ป๊ฒ ๋์๊ฐ๋ ์ง๋ง ๋ณด๊ณ ๋์ด๊ฐ๊ฒ ๋ค.
๋ฐ์ดํฐ ์๋ก๊ณ ์นจ ์ ์ด๋ ํ์ด์ง๋ถํฐ ๋ค์ ๋ก๋ํ ์ง ๊ฒฐ์ ํ๋ ๋ฉ์๋๋ค. ์ฝ๊ฐ savedStateInstance๋๋์ธ๋ฐ, ๊ตฌ์ฑ๋ณ๊ฒฝ์ด ๋ฐ์ํด๋ ๊ฐ์ ์คํฌ๋กค ์์น๋ฅผ ๋ณด๊ณ ์๋ ๊ฒ์ด๋ค. `state.anchorPosition`์ ์ฌ์ฉํ์ฌ ํ์ฌ ํ์๋ ํญ๋ชฉ์ ์์น๋ฅผ ๊ฐ์งํ๊ณ , `closestPageToPosition`์ผ๋ก ํด๋น ์์น์ ๊ฐ์ฅ ๊ฐ๊น์ด ํ์ด์ง๋ฅผ ์ฐพ๋๋ค. ์ด์ ํ์ด์ง์ ๋ค์ ํ์ด์ง ๋๋ ๋ค์ ํ์ด์ง์ ์ด์ ํ์ด์ง๋ฅผ refreshKey๋ก ๋ฐํํ๋ค. refresh ์์ ์ ์ง์ ํด์ฃผ๋ ๊ฒ์ด๋ค. ๊ทผ๋ฐ ์ด๊ฑฐ๋ ๋ง๋ฅ์ ์๋๊ฒ null์ ๋ฐํํด๋ฒ๋ฆฌ๋ฉด ์ฒ์๋ถํฐ ๋ก๋ํด๋ฒ๋ฆฐ๋ค.
์ด๋ ๊ฒ ๋ง๋ PagingSource๋ฅผ Repository์์ Pager๋ฅผ ๊ตฌํํ ๋ ๋ฃ์ด์ค๋ค. api ํธ์ถ์ Datasource์์ ์ฒ๋ฆฌํ๊ณ ์์ด์ ๋ค๋ฅธ ์ฌ๋์ ์ฝ๋๋์ ์กฐ๊ธ ๋ค๋ฅด๋ค.
class FeedRepositoryImpl @Inject constructor(
private val feedDataSource: FeedDataSource
) : FeedRepository {
override fun getPagedFeed(): Flow<PagingData<Feed>> {
return Pager(
config = PagingConfig(
pageSize = PAGE_SIZE,
enablePlaceholders = false,
initialLoadSize = PAGE_SIZE
),
pagingSourceFactory = { FeedPagingSource(feedDataSource) },
).flow
}
companion object {
private const val PAGE_SIZE = 10
}
}
Pager๊ฐ ๊ตฌํ๋ ๋ชจ์ต์ด๋ค. ์ฒ์์ ๋ถ๋ฌ์ธ ํ์ด์ง ์ฌ์ด์ฆ๋ฅผ ์ ํ๊ณ , pagingSourceFactory์ ์๊น ๋ง๋ PagingSource๋ฅผ ์ ๋ฌํด์ flow๋ก ์๋ฉด ๋์ด๋ค.
PagingConfig์ด ์ค์ํ ๋ถ๋ถ์ด๋ค. ์์ฃผ ์ฐ์ด๋ ํ๋กํผํฐ๋ค์ ๊ฐ์ ธ์ ๋ณด๋ฉด
PagingConfig(
pageSize = PAGE_SIZE,
enablePlaceholders = false,
initialLoadSize = PAGE_SIZE * 2,
prefetchDistance = PAGE_SIZE,
maxSize = PAGE_SIZE * 5
)
์ด๊ฑธ ๋ค ์ ์ํด์ค์ผ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ค๊ณ ๋ณผ ์ ์๋ค.
- `pageSize`:
๊ฐ ํ์ด์ง ๋ก๋ ์ ๊ฐ์ ธ์ฌ ํญ๋ชฉ ์๋ฅผ ์๋ฏธํ๋ค. ์ด๊ฒ ์์ผ๋ฉด ์์ ์๋ก ๋คํธ์ํฌ ์์ฒญ ๋น๋๊ฐ ๋์ด๋๋ค. - `enablePlaceholders`:
์์ง ๋ก๋๋์ง ์์ ํญ๋ชฉ์ ๋ํ ํ๋ ์ด์คํ๋ ์ค์ ํ๋ ๋ถ๋ถ์ด๋ค. - `initialLoadSize`:
์ฒ์ ๋ก๋ ์ ๊ฐ์ ธ์ฌ ํญ๋ชฉ์ ์๋ฅผ ์ ์ํ๋ค. ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ๋ฅผ ์ด๊ธฐ ๋ก๋๋ ๊ฐ์ ธ์ค๋ฉด ๋ก๋ฉ์๊ฐ์ด ๊ธธ์ด์ง์ง๋ง ์ ๋นํ ์ด๊ธฐ ๋ก๋ฉ์ ํด๋๋ฉด UX ์ธก๋ฉด์์ ์ข๋ค. ๋ณดํต pageSize์ 2๋ฐฐ๋ 3๋ฐฐ์ ๋๋ก ์ก๋ ๊ฒ ๊ฐ์๋ค. - `prefetchDistance`:
์คํฌ๋กค์ด ๋์ ๋ฟ๊ธฐ ์ ์ ๋ค์ํ์ด์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ๋ ๊ฑธ ์๋ฏธํ๋ค. ์ด๊ฑด pageSize๋ ๊ฐ๊ฒ ๊ฐ์ ธ๊ฐ๋ ๊ฒ ๊ฐ๋ค. - `maxSize`:
๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ ์ต๋ ํญ๋ชฉ์๋ฅผ ์ ์ํ๋ค. ๋งค๋ฒ ๋ถ๋ฌ์ค๊ฒ ํ์ง์๊ณ , ๋ฉ๋ชจ๋ฆฌ ์ต๋์ฌ์ฉ๋์ ๋ฑ ์ ํด๋๊ณ ์ฐ๋ ๋ฐฉ์์ด๋ผ ์ด๋ป๊ฒ ๋ณด๋ฉด ํจ์จ์ ์ด๋ผ๊ณ ํ ์ ์๊ฒ ๋ค.
์ด์ ๋ทฐ๋ชจ๋ธ์์ ์คํธ๋ฆผ์ ๋ฐ์์ค์ผํ๋ค.
private val _pagedFeed = MutableStateFlow<PagingData<Feed>?>(null)
val pagedFeed: StateFlow<PagingData<Feed>?> = _pagedFeed
fun getFeed() {
viewModelScope.launch {
getPagedFeedUseCase().cachedIn(viewModelScope).collectLatest { pagedData ->
_pagedFeed.value = pagedData
}
}
}
PagingData ์ปจํ ์ด๋๋ก ๊ฐ์ธ์ค๋ค. ๋จ์ด๊ฐ Data๋ผ์ ํ๋์ ๋ฐ์ดํฐ๋ผ๊ณ ์๊ฐํ ์ ์๋๋ฐ, ์ปจํ ์ด๋๋ค.
getPagedFeedUseCase๋ ์๊น ๋ง๋ RepositoryImpl์ invokeํด์ฃผ๋ Usecase๋ค. cachedIn์ Paging์์ ์ต์ ํ๋ฅผ ๋์์ฃผ๋ ๋๊ตฌ ์ค ํ๋๋ค. ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ viewModelScope๊ฐ ์ด์์์ ๋ ์บ์ฑํด๋๋ ์ญํ ์ ํ๋ค. ์ด์ ์ด๊ฑธ recyclerview์ PagingDataAdapter์ ๋ฃ์ด์ฃผ๋ฉด ๋๋๋ค.
์ด๋ํฐ๋ถ๋ถ์ ๋ฐ๋ก ๋ญ ๋ณผ๊ฒ ์์ด์ ์๋ตํ๊ฒ ๋ค.
ํ์ด์ง์ ์ฌ์ฉํ๋ฉด ์๋ฌด๋๋ ๋ก๋ํ๋ ์๊ฐ์ด ์๋๋ฐ, ์ด๊ฑธ ์ด์ฉํด์ ๋ก๋ฉ - ๋ก๋ฉ์๋ฃ ํ๋ฉด์ ๊ตฌํํ ์ ์๋ค.
feedAdapter.loadStateFlow.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach { loadStates ->
val isLoading = loadStates.source.refresh is LoadState.Loading
if (!isLoading) loading.dismiss() else loading.show()
}.launchIn(viewLifecycleOwner.lifecycleScope)
์ด๋ ๊ฒ ํด์ฃผ๋ฉด, adapter์ ๋ค์ด๊ฐ LoadState๊ฐ Loading์ผ๋ loading dialog๋ฅผ ๋์์ฃผ๊ณ , ์๋๋ผ๋ฉด dialog๋ฅผ ๊บผ์ค๋ค.
์๋๋ ๊ตฌํํ ๋ชจ์ต์ด๋ค.
๋์์ด ๋๋ค๋ฉด ๋๊ธ์ด๋ ๊ณต๊ฐ ๋ฒํผ ํ ๋ฒ์ฉ ๋๋ฅด๊ณ ๊ฐ์ฃผ์ธ์!
'Android ๐ฅ๏ธ > ์ฝ์งโ๏ธ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ROS - Android ํต์ ํ๊ธฐ(feat. WebSocket, Okhttp3) (0) | 2024.08.27 |
---|---|
MLKit ํ์ฉํ๊ธฐ - FaceMeshDetection (0) | 2024.08.26 |
lifecycleScope์ viewLifecycleOwner.lifecycleScope (0) | 2024.07.27 |
RecyclerView์์ ๋น๊ธฐ๋ ํ๋ ์ฒ๋ฆฌ - edgeEffectFactory (0) | 2024.07.24 |
ViewType์ ๋๋ RecyclerView๋ฅผ ๊ตฌ์ฑํ๊ธฐ(feat. SealedClass) (0) | 2024.07.23 |