๋ชฉ์ฐจ
๋ค์ด๊ฐ๊ธฐ ์ ์
์๋์ผ๋ก ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ๋ค๊ฐ Paging3์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ์ด๋ค ๊ฒ์ผ๊น๋ฅผ ์ฐพ์๋ณด์๋ค.
๊ทธ๋ฌ๋๋ ๋ด๊ฐ ์๋์ผ๋ก ๊ตฌํํ๋ ๊ฑฐ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๊ณ ์์๋ค!!!!
1. ์คํฌ๋กคํ๋ฉด์ ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ ํธ์ถ
2. ์๋ก ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์ ๋ณด์ฌ์ฃผ๊ธฐ
3. ๋ง์ง๋ง ํ์ด์ง์ ์ค๋ฉด ์คํ
4. ๋ฐ์ดํฐ ๋ก๋ฉ ์ถ๊ฐ
๋ฑ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ๋ ๊ฐ๋จํ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ๋ชจ๋ ์ ๊ณตํด์ฃผ์๋ค.
์์ผ๋ก๋ ์ฌ์ฉ์ ํ์ด์ง๋ ํ๋ก์ ํ๋ฉด ๋ฑ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํด์ผํ ๋๊ฐ ๋ง์๊ณ ์ฑ ์ฌ์ฉ์๊ฐ ๋ง์์ง์ ๋ฐ๋ผ ์๋์ผ๋ก ๋ชจ๋ ๊ตฌํํ๊ธฐ ๋ณด๋ค๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๋ฉด ํจ์จ์ ์ด๊ณ ์ค๋ฅ ๋ฐ์๋ ์ค์ผ ์ ์์๊ฑฐ๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
๋ฐ๋ผ์ ๊ธฐ์กด ์๋์ผ๋ก ๊ตฌํํ๋ ๊ฒ์ ์ ๊ด๋ จ ์ํ ํฌ์คํฐ๋ฅผ ํ์ํด์ฃผ๋ ๋ถ๋ถ์ Paging3๋ฅผ ์ ์ฉํด๋ณด์๋ค.
์ฒ์ Paging3๋ฅผ ์ด์ฉํ๋ ค๊ณ ํ์ ๋์๋ ๊ฐ๋ ์ด ์กฐ๊ธ ์ด๋ ค์ ์ดํดํ๋๋ฐ ์๊ฐ์ด ๋ค์ ๊ฑธ๋ ธ๋ค.
๋ฐ๋ผ์ ์ดํดํ๋ ๊ฐ๋ ๋ถํฐ ์ ๋ฆฌํ๊ณ ๋์ด๊ฐ๋ณด๋ ค ํ๋น
Paging3 ์ดํด๋ณด๊ธฐ
Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๊ธฐ ์ํด์๋ 3๊ฐ์ง ๋ ์ด์ด์์ ๊ตฌ์ฑ์์๋ฅผ ๊ตฌํํด์ค์ผํ๋ค.
1. Repository ๊ณ์ธต
Repository ๊ณ์ธต์์์ ๊ธฐ๋ณธ ๊ตฌ์ฑ์์๋ PagingSource์ด๋ค.
PagingSource ๊ฐ์ฒด๋ ๋ฐ์ดํฐ ์์ค์ ํด๋น ์์ค์์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๋ ๋ฐฉ๋ฒ์ ์ ์ํด์ค๋ค.
2. ViewModel ๊ณ์ธต
Pager ๊ตฌ์ฑ ์์๋ PagingSource ๊ฐ์ฒด ๋ฐ PagingConfig ๊ตฌ์ฑ ๊ฐ์ฒด๋ฅผ ๋ฐํ์ผ๋ก ๋ฐ์ํ ์คํธ๋ฆผ์ ๋ ธ์ถ๋๋
PagingData๊ฐ์ฒด๋ฅผ ๊ตฌ์ฑํ๊ธฐ ์ํ ๊ณต๊ฐ Api ๋ฅผ ์ ๊ณตํด์ค๋ค.
๋ทฐ๋ชจ๋ธ ๊ณ์ธต์ UI์ ์ฐ๊ฒฐํ๋ ์์๊ฐ PagingData์ด๋ค.
PagingData ๊ฐ์ฒด๋ ํ์ด์ง๋ก ๋๋ ๋ฐ์ดํฐ์ ์ค๋ ์ท์ ๋ณด์ ํ๋ ์ปจํ ์ด๋๋ก PagingSource ๊ฐ์ฒด๋ฅผ ์ฟผ๋ฆฌํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ๋ค.
3. UI ๊ณ์ธต
UI ํ์ด์ง์ ๊ธฐ๋ณธ ๊ตฌ์ฑ ์์๋ ํ์ด์ง๋ก ๋๋ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ Recyclerview ์ด๋ํฐ์ธ PagingDataAdapter์ด๋ค.
Repository ๊ณ์ธต - PagingSource ๊ตฌํํด๋ณด๊ธฐ
์ฐ์ PagingSource๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์๋
1. PagingSource<Key, Value>๋ฅผ ํ์ฅํด์ผํ๋ค.
์ด ๋ Key๋ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋๋ฐ ์ฌ์ฉ๋๋ ์๋ณ์๋ฅผ ์๋ฏธํ๊ณ , Value๋ ๋ก๋ํ ๋ฐ์ดํฐ์ ํ์ ์ ์๋ฏธํ๋ค.
๋๋ ํ์ด์ง ๋ฒํธ๋ฅผ ํตํด MovieContentResult ํ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ์์ฑํด์ฃผ์๋ค.
data class MovieContentResultWithIndex(val index: Int, val result: MovieContentResult)
class MoviePagingSource(
private val searchMovieListUseCase: SearchMovieListUseCase,
private val query: String?,
) : PagingSource<Int, MovieContentResultWithIndex>() {
// key : Int : ํ์ด์ง ๋ฒํธ
// value : Result : ์ํ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ ๋ฐ์ดํฐ ํด๋์ค
2. ์ด์ load ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํด์ค์ผํ๋ค.
load ๋ฉ์๋ : ๋ฐ์ดํฐ๋ฅผ UI๋ก ๋ก๋ํ๋ ์ญํ ์ ํ๋ค.
- ์ฐ์ ์์ ํ์ด์ง๋ฅผ ์ง์ ํด์ฃผ์ด์ผํ๋ค. start๋ผ๋ ๋ณ์์ ์์ ํ์ด์ง๋ฅผ ๋ํ๋ผ ๋ณ์๋ฅผ ๋ด์์ฃผ์๋ค.
- ๋ฐ์ดํฐ๋ searchMovieListUseCase๋ฅผ ํตํด ์ ์ํด๋ ์ ์ฆ ์ผ์ด์ค์ ๊ฒ์์ฐฝ์ ์์ฑ๋ ๊ฒ์์ด์ ํ์ด์ง๋ฅผ ๋๊ฒจ์ ๋ฐ๋๋ก ํ๋ค.
- ๋ก๋ ์์ ์ ๊ฒฐ๊ณผ๋ฅผ ๋ํ๋ด๊ธฐ ์ํด์ LoadResult.Page์ LoadResult.Error์ ์ฌ์ฉํ๊ณ ์๋ค.
LoadResult.Page๋ ๋ก๋ ์์ ์ด ์ฑ๊ณตํ์ ๊ฒฝ์ฐ ๋ฐํ๋๋ ๊ฐ์ฒด์ด๊ณ , LoadResult.Error๋ ๋ก๋ ์์ ์ด ์คํจํ์ ๊ฒฝ์ฐ ๋ฐํ๋๋ ๊ฐ์ฒด์ด๋ค.
์ฑ๊ณตํ์ ๊ฒฝ์ฐ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์๋์ ๊ฐ์ด
// PagingSource์ ์์ฑ์์ ์ ๊ณต๋ ๋งค๊ฐ๋ณ์๋ฅผ load ๋ฉ์๋์ ๋๊ฒจ์ฃผ์ด ์ ํฉํ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ค.
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MovieContentResultWithIndex> {
val start = params.key ?: 1
return try {
val movieRequest = query?.let { MovieRequest(it, start) }
val response = searchMovieListUseCase(movieRequest)
// ๋ฐ์ดํฐ์ ์ธ๋ฑ์ค๋ฅผ ๋ฐ์ํด์ฃผ๊ธฐ ์ํ ์ฝ๋
val movieContentResultWithIndices =
response.first().mapIndexed { index, result ->
MovieContentResultWithIndex((start - 1) * params.loadSize + index, result)
}
/*
LoadResult : ๋ก๋ ์์
์ ๊ฒฐ๊ณผ๋ฅผ ํฌํจ
๋ก๋์ ์ฑ๊ณตํ์ ๊ฒฝ์ฐ LoadResult.Page๋ฅผ ๋ฐํ
๋ก๋ ์คํจ ์ LoadResult.Error๋ฅผ ๋ฐํํ๋ค.
*/
/*
LoadResult.Page(
data : ๊ฐ์ ธ์จ ํญ๋ชฉ์ List๋ก List<Value> ํํ์ด๋ค.
prevKey : ํ์ฌ ํ์ด์ง ์์ ํญ๋ชฉ์ ๊ฐ์ ธ์์ผํ๋ ๊ฒฝ์ฐ load ๋ฉ์๋์์ ์ฌ์ฉํ๋ ํค
nextKey : ํ์ฌ ํ์ด์ง ๋ค์ ํญ๋ชฉ์ ๊ฐ์ ธ์์ผํ๋ ๊ฒฝ์ฐ load ๋ฉ์๋์์ ์ฌ์ฉํ๋ ํค
*/
}
}
ํฌ๊ฒ data, prevKey, nextKey์ ๋ํด ๊ตฌํํด์ฃผ์ด์ผํ๋๋ฐ
data๋ ๊ฐ์ ธ์จ ํญ๋ชฉ์ List๋ฅผ ๋งํ๋ค.
1 ํ์ด์ง์์ 20๊ฐ์ ๋ฐ์ดํฐ๊ฐ ์จ๋ค๋ฉด 0๋ถํฐ 19๋ฒ์งธ ์์น๊น์ง ์ํ ํฌ์คํฐ๊ฐ ํ์๋ ๊ฒ์ด๋ค.
2 ํ์ด์ง์์๋ 20๊ฐ์ ๋ฐ์ดํฐ๊ฐ ์จ๋ค๋ฉด 20๋ฒ์งธ ์์น๋ถํฐ 29๋ฒ์งธ๊น์ง ์ํ ํฌ์คํฐ๊ฐ ํ์๋์ด์ผํ๋ค.
๋ฐ๋ผ์ ์ธ๋ฑ์ค๋ฅผ (start-1) * params.loadSize + index๋ก ๊ณ์ฐํ์ฌ ๋ชจ๋ ํ์ด์ง๊ฐ ์๋ง์ ์์น์ ํ์๋๋๋ก ํ์๋ค. ์ด๋ ๊ฒ ์ธ๋ฑ์ค์ ํจ๊ป ๋ฐ์ดํฐ๋ฅผ ๋ด๊ณ ์๋ ๋ฐ์ดํฐ movieContentResultWithIndices๋ฅผ data ๋ถ๋ถ์ ์์ฑํด์ฃผ์๋ค.
์ด ์ฝ๋๋ฅผ ์์ฑํด์ฃผ์ง ์์์ 1ํ์ด์ง์ ๋ฐ์ดํฐ๊ฐ 0๋ฒ์งธ ์์น์ ๋ก๋๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ณ ์ด๊ฑฐ๋๋ฌธ์ ๋ฉฐ์น ์ ํด๋งธ๋ค ใ ใ ใ ๊ผญ ๋ฐ์ํด์ ์์ฑํด์ฃผ์!!!
๊ธฐ์กด์๋ ์๋์ฒ๋ผ ์์ฑํด์ฃผ์์๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์์์ ๋ค์ ์ถ๊ฐ์ถ๊ฐ๋ฅผ ์์ผ์ฃผ๋ ์ค ์์์ ใ ..ใ
LoadResult.Page(
data = response.first(),
)
์ ์ด์ฃผ์ง ์์ ๊ฒฝ์ฐ ์๋์ ์์๊ณผ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ง์ดํ๊ฒ ๋ ๊ฒ์ด๋ค..
์ด์ ๋ฅผ ์ฐพ์๋ณด๋ ์์ฒ๋ผ reponse.first() ๋ง ๋๊ฒจ์ฃผ๊ฒ ๋๋ฉด ํด๋น ๋ฐ์ดํฐ๊ฐ ๋ฐฐ์น๋์ด์ผํ๋ ์์น์ ๋ํ ์ธ๋ฑ์ค๋ฅผ ์กฐ์ ํ์ง ์๊ณ ํ์ฌ ํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๊ฒ ๋๋ค๊ณ ํ๋ค.
๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๋ ํญ๋ชฉ์ ๋ณด์ฌ์ค ๋ 0๋ถํฐ ์์ํ๋ ์ธ๋ฑ์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ ํ์ด์ง์ ํญ๋ชฉ์ ๋ํ ์ธ๋ฑ์ค๋ฅผ ์๋์ผ๋ก ์กฐ์ ํด์ฃผ์ง ์๋๋ค๊ณ ํ๋ค.
๋ฐ๋ผ์ ํ์ฌ ํ์ด์ง์ ํญ๋ชฉ์ ๋ํ ์ธ๋ฑ์ค๋ฅผ ๊ณ์ฐํ์ง ์๊ณ ์ ํ์ด์ง์ ํญ๋ชฉ์ ๋ก๋ํ๋ฉด ๊ธฐ์กด์ ํญ๋ชฉ์ ๋ฎ์ด์ฐ๊ฒ ๋ ๊ฒ์ด๋ค..!
๊ผญ ์์ฑํด์ฃผ๊ธฐ๋ฅผ ๊ธฐ์ตํ๋ฉด์ ๋ค์ prevKey์ nextKey์ ๋ํด์ ์์ฑํด๋ณด์
๋๋ ์๋ฒ์์ ํ์ด์ง๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์
ํ์ฌ ํ์ด์ง ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด load ๋ฉ์๋์์ ์ฌ์ฉ๋๋ prevKey์๋ ํ์ฌ ํ์ด์ง -1์ ๋๊ฒจ์ฃผ๊ณ ,
ํ์ฌ ํ์ด์ง ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด load ๋ฉ์๋์์ ์ฌ์ฉํ๋ nextKey์๋ ํ์ฌ ํ์ด์ง์ +1์ ๋๊ฒจ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
ํ์ด์ง ๋ฒํธ๊ฐ ๊ธฐ์ค์ด ์๋๋ผ ์์ดํ ์ ์ธ๋ฑ์ค๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ฐ์ ธ์จ๋ค๋ฉด ์ฃผ์์ ์์ฑํ ๋ด์ฉ์ ๋ฐ์ํ์ฌ ์์ฑํด์ฃผ๋ฉด ๋๋ค.
๋ง์ง๋ง์ผ๋ก ๋ก๋๊ฐ ์คํจํ์ ๊ฒฝ์ฐ ์ฝ๋๋ ์์ฑํด์ฃผ๋ฉด PagingSource๋ ๋์ด๋ค!
LoadResult.Page(
data = movieContentResultWithIndices,
/*
1. ํ์ด์ง ๋ฒํธ๋ฅผ ๊ธฐ์ค์ผ๋ก ํญ๋ชฉ ๋ชฉ๋ก์ ํ์ด์ง๋ฅผ ๋งค๊ธฐ๋ ๊ฒฝ์ฐ,
preKey : ํ์ฌ ํ์ด์ง ๋ฒํธ -1
nextKey : ํ์ฌ ํ์ด์ง ๋ฒํธ +1
2. ์์ดํ
์ธ๋ฑ์ค ๊ธฐ์ค์ผ๋ก ํ์ด์ง๋ฅผ ๋งค๊ธฐ๋ ๊ฒฝ์ฐ
ํ์ด์ง ํฌ๊ธฐ : params.loadsize
prevKey : ๋ก๋ํ ์ฒซ๋ฒ ์งธ ํญ๋ชฉ์ ์ธ๋ฑ์ค (์ด๊ธฐ : 0)
0 ~ 19 ๊น์ง ์ฒซ๋ฒ์งธ ํ์ด์ง์ ๋ก๊ทธ๋์๋ค๋ฉด
20์ด ์์ ์ธ๋ฑ์ค๊ฐ ๋๋ค.
nextKey : ํ์ฌ ํญ๋ชฉ์ ๋ก๋ํ ํ ๋ง์ง๋ง์ ๋ก๋๋ ํญ๋ชฉ์ ์ธ๋ฑ์ค
์ฆ ์์ ์ธ๋ฑ์ค + ๋ก๋๋ ์์ดํ
์ ์ฌ์ด์ฆ
*/
prevKey = if (start == 1) null else start - 1,
nextKey = if (response.first().isEmpty()) null else start + 1,
)
} catch (e: Exception) {
// ๋คํธ์ํฌ ์๋ฌ์ ๊ฐ์ ๋ด์ฉ์ ์ฒ๋ฆฌํ๊ณ LoadResult.Error์ ๋ฐํํ๋ค.
e.printStackTrace()
LoadResult.Error(e)
}
ViewModel - Flow<PaginData<Value>> ๊ตฌํ
์ด์ ViewModel์์ PagingSource๋ก๋ถํฐ ํ์ด์ง๋ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ์ค์ ํด์ฃผ์.
๋ฐ์ดํฐ ์คํธ๋ฆผ๊ณผ ๋ฐ์ํ ์คํธ๋ฆผ์ ๋ํด์ ์ ๋ฆฌํ ๋ด์ฉ์ ์๋๋ฅผ ์ฝ์ด๋ณด์.
/*
๋ฐ์ดํฐ ์คํธ๋ฆผ๊ณผ ๋ฐ์ํ ์คํธ๋ฆผ์ด๋?
1. ๋ฐ์ดํฐ ์คํธ๋ฆผ : ์ฌ์ฉ์๊ฐ ๋ชฉ๋ก์ ์คํฌ๋กคํ ๋ ์ง์์ ์ผ๋ก ๋ฐฉ์ถ๋๋ ํ์ด์ง๊ฐ ๋งค๊ฒจ์ง ํ์ด์ง(PagingData)์ ํ๋ฆ
- ์ด๋ Flow๋ฅผ ์ฌ์ฉํ์ฌ ์ฒ๋ฆฌ๋๋ค.
- ๋น๋๊ธฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ์ฝ๋ ๋ฐ ์คํธ๋ฆผ์ด ์ ๊ทน์ ์ผ๋ก ์์ง๋ ๋๋ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ์ฌ์ฉ
- Pager์ ์ํด ๋ฐ์ดํฐ ์คํธ๋ฆผ์ด ์ค์ ๋๊ณ Flow<PagingData<MovieContentResultWithIndex>>๋ก ๋ฐํ๋๋ค.
2. ๋ฐ์ํ ์คํธ๋ฆผ : ๋ฐ์ดํฐ ์คํธ๋ฆผ์ด ์ฌ์ฉ์ ์์
(์คํฌ๋กค) ๋๋ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ์ ๊ฐ์
๋ณ๊ฒฝ ์ฌํญ์ ๋ฐ์ํ๋ ๋ฐฉ์
Flow : ๋ฐ์ํ ์คํธ๋ฆผ , ์ฌ์ฉ์๊ฐ UI์ ์ํธ์์ฉํ ๋ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ ๋ด๋ณด๋ธ๋ค.
Pager ํด๋์ค๋ PaginSource์์ PagingData ๊ฐ์ฒด์ ๋ฐ์ํ ์คํธ๋ฆผ์ ๊ฐ์ ธ์ค๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ Flow,LiveData, RxJava์ ์ฌ๋ฌ ์คํธ๋ฆผ ํ์ ์ฌ์ฉ์ ์ง์ํ๋ค. ๋๋ ๊ธฐ์กด์๋ Flow๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ ๊ทธ๋๋ก Flow๋ฅผ ์ฌ์ฉํด์ฃผ์๋ค.
๋ฐ์ํ ์คํธ๋ฆผ์ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํด์๋ ์๋ 2๊ฐ์ง๋ฅผ Pager ๊ฐ์ฒด์ ์ ๊ณตํด์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค์ผํ๋ค.
1. PagingConfig ๊ฐ์ฒด
2. PagingSource ๊ตฌํ์ ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ์ Pager์ ์๋ ค์ฃผ๋ ํจ์
private fun getMoviePagingData(query: String): Flow<PagingData<MovieContentResultWithIndex>> {
return Pager(
config =
PagingConfig(
// ์ด๋ป๊ฒ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๊ฒ์ธ์ง
// ํ์ด์ง๋ง๋ค ๋ณด์ฌ์ค ์์ดํ
์ ์ (๋์ผํ์ง ์์๋ ๋จ)
pageSize = 20,
enablePlaceholders = false,
),
// PagingSource ๊ตฌํ์ ์ธ์คํด์ค๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ์ ์๋ ค์ฃผ๋ ํจ์
pagingSourceFactory = {
MoviePagingSource(searchMovieListUseCase, query)
},
// cachedIn : ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๊ณต์ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค๊ณ ์ ๊ณต๋ ์ฝ๋ฃจํด์ค์ฝํ๋ฅผ ์ฌ์ฉํ์ฌ
// ๋ก๋๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ํ๋ค.
).flow.cachedIn(viewModelScope)
// Pager ๊ฐ์ฒด๋ PagingSource ๊ฐ์ฒด์์ load ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ LoadParams ๊ฐ์ฒด๋ฅผ
// ์ ๊ณตํ๊ณ LoadResult ๊ฐ์ฒด๋ฅผ ๋ฐํ๋ฐ๋๋ค.
}
UI ๊ณ์ธต - ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์ด๋ํฐ ์ ์ํ๊ธฐ
์ด์ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์ด๋ํฐ๋ฅผ ์ค์ ํด์ค์ผํ๋ค.
/*
RecyclerView๋ฅผ ํตํด Pagingํ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์ PagingDataAdapter๋ฅผ ํ์ฅํด์ค๋ค.
PagingDataAdapter๋ฅผ ํ์ฅํ์ฌ MoviePosterAdapter ์ด๋ํฐ๋ฅผ ๋ง๋ค์ด MovieContentResultWithIndex ํ์
์ ๋ชฉ๋ก์ ๋ํ
RecyclerView ์ด๋ํฐ๋ฅผ ์ ๊ณตํ๊ณ MoviePosterViewHolder ,MovieBackgroundViewHolder๋ฅผ ๋ทฐํ๋๋ก ์ฌ์ฉํ๊ณ ์๋ค.
*/
class MoviePosterAdapter(private val context: Context) : PagingDataAdapter<MovieContentResultWithIndex, MoviePosterAdapter.MoviePosterViewHolder>(MovieDiffCallback()) {
inner class MoviePosterViewHolder(private val binding: MoviePosterItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.ivMoviePoster.setOnClickListener {
Timber.d("clicked : $position")
val movieId = getItem(position)?.result?.id
val movieName = getItem(position)?.result?.title
if (movieId != null && movieName != null) {
onItemClickListener?.onItemClick(movieId, movieName)
}
}
}
fun bind(movie: MovieContentResultWithIndex) {
Glide.with(context)
.load("https://image.tmdb.org/t/p/original${movie.result.posterPath}")
.into(binding.ivMoviePoster)
}
}
๊ธฐ์กด์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์ด๋ํฐ์ ๋์ผํ๊ฒ onCreateViewHolder () ๋ฐ onBindViewHolder ()๋ฉ์๋๋ฅผ ์ฌ์ ์ํด์ค์ผํ๋ค.
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MoviePosterViewHolder {
val binding = MoviePosterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MoviePosterViewHolder(binding)
}
override fun onBindViewHolder(
holder: MoviePosterViewHolder,
position: Int,
) {
val moviePath = getItem(position)
if (moviePath != null) {
holder.bind(moviePath)
}
}
๊ธฐ์กด ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์ด๋ํฐ์ ๋ค๋ฅธ ์ ๋ 2๊ฐ์ง ์๋ค.
1. DiffUtil.ItemCallBack์ ์ง์ ํด์ค์ผํ๋ค.
2. ๊ธฐ์กด์ ๊ฒฝ์ฐ ๋ฐ์ดํฐ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ง๊ณ ์์ง๋ง PagingDataAdapter์ ๊ฒฝ์ฐ getItem(position)์ ํตํด ์์ดํ ์ ์ ๊ทผํ ์ ์๋ค.
DiffUtil.ItemCallBack์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๊ฐ ๋ณ๊ฒฝ๋ ํญ๋ชฉ๋ง ์์ ํ์ฌ ์ ๋ฐ์ดํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ์ต์ ํํด์ค๋ค.
๊ทธ๋์ DiffUtil์ ์ฌ์ฉํด๋ณด๊ณ ์ถ์๋๋ฐ ์ด๋ฒ๊ธฐํ์ ์ด๋ ๊ฒ ์ฌ์ฉํด๋ณผ ์ ์์ด ์ข์๋ค : D
/*
DiffUtil.ItemCallback ์ง์
*/
private class MovieDiffCallback : DiffUtil.ItemCallback<MovieContentResultWithIndex>() {
override fun areItemsTheSame(
oldItem: MovieContentResultWithIndex,
newItem: MovieContentResultWithIndex,
): Boolean {
return oldItem.index == newItem.index
}
override fun areContentsTheSame(
oldItem: MovieContentResultWithIndex,
newItem: MovieContentResultWithIndex,
): Boolean {
return oldItem.index == newItem.index
}
}
๋ ๊ฐ์ง ๋ฉ์๋๋ฅผ ๊ตฌํํด์ค์ผํ๋๋ฐ areItemsTheSame์ areContentsTheSame์ด๋ค.
areItemsTheSame๋ ๊ณ ์ ์๋ณ์(์๋ฅผ ๋ค์ด index)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ ํญ๋ชฉ์ด ๋์ผํ์ง ํ์ธํด์ค๋ค.
areContentsTheSame๋ ๋ ํญ๋ชฉ์ ๋ด์ฉ์ด ๋์ผํ์ง ํ์ธํด์ค๋ค.
๋๋ ๋ฐ์ดํฐ์ ํจ๊ป ์ธ๋ฑ์ฌ ์ ๋ณด๋ฅผ ๋ด์์คฌ๊ธฐ ๋๋ฌธ์ ์ฌ๊ธฐ์๋ index๋ฅผ ๊ธฐ์ค์ผ๋ก ํญ๋ชฉ์ ๋น๊ตํด์ฃผ์๋ค.
UI์ ํ์ด์ง๋ ๋ฐ์ดํฐ๋ฅผ ํ์ํด์ฃผ์
์ด์ PagingSource , PagingData ์คํธ๋ฆผ, PagingDataAdapter๊น์ง ๋ชจ๋ ๊ตฌํํ๋ค. ๋๋์ด UI์์ ํ์ด์ง๋ ๋ฐ์ดํฐ๋ฅผ ํ์ํ ์ฐจ๋ก์ด๋ค.
๋จผ์ PagingDataAdapter ๋ฅผ ํ์ฅํด์คฌ๋ MoviePosterAdapter ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ฃผ๊ณ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์ ์ ์ฉํด์ฃผ์๋ค.
private val moviePosterAdapter by lazy {
context?.let { MoviePosterAdapter(it) }
}
binding.movieRecyclerView.adapter = moviePosterAdapter
๊ทธ ํ ๋ทฐ๋ชจ๋ธ์์ ์์ฑํด์ค PagingData ์คํธ๋ฆผ์ ๊ด์ฐฐํ๊ณ ์์ฑ๋ ๊ฐ์ ์ด๋ํฐ์ submitData() ๋ฉ์๋์ ์ ๋ฌํด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค
// PagingData ์คํธ๋ฆผ์ ๊ด์ฐฐํ๊ณ ์์ฑ๋ ๊ฐ ๊ฐ์ ์ด๋ํฐ์ submitData() ๋ฉ์๋์ ์ ๋ฌํ๋ค.
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.moviePosterPathFlow.collectLatest {
moviePosterAdapter?.submitData(it)
}
}
}
์ฌ๊ธฐ์ ์ viewLifecyclerOwner.lifecycleScope.launch๋ก ์์ฑํด์ฃผ์๋์ง๊ฐ ๊ถ๊ธํ๋ค๋ฉด ์๋ ๋ด์ฉ์ ์ฝ์ด๋ณด์!!
/*
Fragment๋ viewLifecycleOwner.lifecycleScope๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
- lifecycleScope.launch
- ์ฝ๋ฃจํด์ ํ๋๊ทธ๋จผํธ์ ์๋ช
์ฃผ๊ธฐ์ ์ฐ๊ฒฐํ๋ค.
- ํ๋๊ทธ๋จผํธ๊ฐ ์ ํจํ ์ํ์ ์๋ ํ ์ฝ๋ฃจํด์ด ๊ณ์ ์คํ๋๋ค.
- ํ๋๊ทธ๋จผํธ์ ๋ทฐ๊ฐ ์ญ์ ๋์ด๋ ๊ณ์ ์คํ๋ ์ ์๋ค.
- ํ๋๊ทธ๋จผํธ๊ฐ ๋ทฐ์ ์์กดํ์ง ์๋ ์์
(๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
)์ ์ ํฉ
-viewLifecycleOwner.lifecycleScope.launch :
- ์ฝ๋ฃจํด์ ํ๋๊ทธ๋จผํธ ๋ทฐ์ ์๋ช
์ฃผ๊ธฐ์ ์ฐ๊ฒฐํ๋ค.
- ๋ทฐ๊ฐ ํ๊ดด๋ ๋ ์ฝ๋ฃจํด์ด ์๋์ผ๋ก ์ทจ์๋๋ค.
- ์ผ๋ฐ์ ์ผ๋ก ํ๋๊ทธ๋จผํธ๊ฐ ์ค์ง๋๊ฑฐ๋ ๊ต์ฒด๋ ๋ ๋ฐ์ํ๋ค.
- UI ๊ด๋ จ ์์
์ด๋ UI๋ฅผ ์
๋ฐ์ดํธํ๋ (StateFlow๋ฅผ ๊ด์ฐฐํ ๋) ์ ํธ๋๋ฉฐ, ๋ทฐ๊ฐ ์ ํจํ ์ํ์ผ ๋ ์
๋ฐ์ดํธ๊ฐ ์ด๋ฃจ์ด์ง๋๋ก ํจ.
๊ทธ๋ผ ์ ์กํฐ๋นํฐ์์๋ ๊ทธ๋ฅ lifecycleScope.launch์ ์ฌ์ฉํด๋ ๋ ๊น?
- ์กํฐ๋นํฐ๋ ๋ณ๋์ ๋ทฐ ์๋ช
์ฃผ๊ธฐ๊ฐ ์๋ค.
- ์กํฐ๋นํฐ์ ๋ทฐ๋ ์กํฐ๋นํฐ
์ด๋ ๊ฒ๋ง ํ๋ฉด ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๋ฅผ ํตํด ๋ฐ์ดํฐ ์์ค์์ ํ์ด์ง๋ ๋ฐ์ดํฐ๋ฅผ ํ์ํ๊ณ ๋ค๋ฅธ ํ์ด์ง์ ๋ฐ์ดํฐ๊น์ง ์๋์ผ๋ก ๋ก๋ํด์ ๋ณผ ์ ์๊ฒ๋๋ค..
์ด์ ์ฒ๋ผ ์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ํฐ์ ์๋ ค์ฃผ๋ ์ฝ๋๋ฅผ ์งค ํ์๋, ๋ง์ง๋ง ๋ฐ์ดํฐ์์ ์๋ ค์ค ํ์๋ ์์ด์ก๋ค๐ฅฐ
๋ก๋ฉ ์ถ๊ฐํ๊ธฐ
์ด์ ์๋ ๋ก๋ฉ์ ์ถ๊ฐํด์ฃผ๊ธฐ ์ํด์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์์ ๋ฉํฐ๋ทฐ๋ฅผ ํตํด ๋ก๋ฉ๋ง์ ์ํด ๋ทฐํ๋๋ฅผ ๊ตฌํํด์ ์ ์ฉํด์ค ์ ์์์ง๋ง Paging ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด์๋ ์ฝ๋๋ก ๋ฐ๋ก ์ ์ฉํด์ค ์ ์๋ค ๐
๋จผ์ ๋ ์ด์์ ์์์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์๋์ ๋ก๋ฉ๋ฐ๋ฅผ ์ถ๊ฐํด์ค๋ค.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movie_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:horizontalSpacing="5dp"
app:layoutManager="GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/movie_progress_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintVertical_bias="0.0"
app:spanCount="3"
tools:listitem="@layout/movie_poster_item" />
<com.google.android.material.progressindicator.LinearProgressIndicator
app:indicatorColor="@color/primary60"
android:id="@+id/movie_progress_bar"
android:layout_width="0dp"
android:layout_height="20dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent" />
๊ทธ ํ ํ๋๊ทธ๋จผํธ์์ ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋์ด๋ค!!
// moviePosterAdapter์ loadStateFlow ์์ฑ์์ ๊ฐ์ ์์งํ๋ค.
// ๋ฐ์ดํฐ์ ํ์ฌ ๋ก๋์ํ(๋ก๋ ์ค, ์ฑ๊ณต์ ์ผ๋ก ๋ก๋๋์๋์ง, ์ค๋ฅ๊ฐ ๋ฐ์ํ๋์ง)๋ฅผ ๋ด๋ณด๋ธ๋ค.
// collectLatest : ํ๋ฆ์์ ๋ฐฉ์ถ๋ ์ต์ ๊ฐ์ ์์งํ๋ค.
// ๊ฐ์ฅ ์ต๊ทผ ์ํ๋ง ์ค์ํ UI ์
๋ฐ์ดํธ์์ ์ ์ฉํ๋ค.
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
moviePosterAdapter?.loadStateFlow?.collectLatest {
binding.movieProgressBar.isVisible = it.source.append is LoadState.Loading
}
}
}
์ฌ๊ธฐ์ loadStateFlow๋ ์ด๋ํฐ์ ํ์ฌ์ ๋ก๋ฉ ์ํ๋ฅผ ๋ด๋ณด๋ด๋ Flow์ด๋ค.
์ํ์๋ ์๋ 3๊ฐ์ง๊ฐ ์๋ค.
LoadState.Loading : ๋ก๋ฉ ์ค, ํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ค
LoadState.NotLoading : ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ก๋๋ ๊ฒฝ์ฐ
LoadState.Error : ๋ฐ์ดํฐ ๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
it.source๋ LoadState ํ์ ์ผ๋ก PagingSource์์ ๋ก๋๋ ๋ฐ์ดํฐ์ ์ํ๋ฅผ ๋งํ๋ค.
์ํ๋ ์๋ 3๊ฐ์ง๋ก ๋๋๋ค๊ณ ํ๋ค.
it.source.append : ๋ฐ์ดํฐ์ ์ถ๊ฐ์์
์ํ๋ฅผ ๋ํ๋ธ๋ค. ๋ชฉ๋ก ๋์ ๋ ๋ง์ ํญ๋ชฉ์ ๋ก๋ํ๊ณ ์๋ ๊ฒฝ์ฐ(์๋ฅผ ๋ค์ด ๋ค์ ํ์ด์ง ๋ก๋)
it.source.prepend : ํ์ฌ ํญ๋ชฉ ์ด์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๊ฒ์ผ๋ก ์ฌ์ฉ์๊ฐ ๋ชฉ๋ก์ ๋ค์ ์๋ก ์คํฌ๋กคํ๋ ๊ฒฝ์ฐ ์ด์ ํญ๋ชฉ์ ๋ก๋ํ๊ณ ํ์ํ๋ ค๊ณ ํ ๋ prepend๊ฐ ํธ๋ฆฌ๊ฑฐ๋๋ค.
it.source.retry : ์ ์ฒด ๋ฐ์ดํฐ ์ธํธ๋ฅผ ๋ค์ ๋ก๋ํ๋ ๊ฒ์ด๋ค.
๋๋ append๋ฅผ ํตํด ๋ฐ์ดํฐ ์ถ๊ฐ์์ ์ ๋ํ ๋ก๋ฉ์ ์ถ๊ฐํด์ฃผ์๋๋ฐ, prepend์ ๋ํ ์์ฑ๋ ์ถ๊ฐํด์ฃผ๋ฉด ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ ๋ก๋ฉ ์ํ๋ฅผ ์ ์ ์์ด ๋์ฑ ์ข์ ๊ฒ ๊ฐ๋ค : )
์ ์ด์ ์ด๋ ๊ฒ ๊ตฌํํ ๋ฌดํ ์คํฌ๋กค ๊ฒฐ๊ณผ๋ฌผ์ ๋ณด๊ณ ๋ง๋ฌด๋ฆฌํด๋ณด์ฅ
์๋์ผ๋ก ๊ตฌํํ ๋์ ๋น๊ต
binding.movieRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int,
) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as GridLayoutManager).findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter?.itemCount?.minus(1)
// ์คํฌ๋กค์ด ๋์ ๋๋ฌํ๋์ง ํ์ธ
if (lastVisibleItemPosition == itemTotalCount) {
currentPage++
viewModel.handleEvent(MovieSelectEvent.LoadNextPageMovie(queryText, currentPage))
}
}
},
)
binding.movieSearchView.setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
// ์ฒ์ ๊ฒ์ ์์๋ 1ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ด
queryText = query
currentPage = 1
viewModel.handleEvent(MovieSelectEvent.SearchMovie(queryText, currentPage))
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
moviePosterAdapter?.initializePosterUriList()
viewModel.handleEvent(MovieSelectEvent.InitializeMovieList)
return true
}
},
)
์๋์ผ๋ก ๊ตฌํํ ๊ฒฝ์ฐ ์๋๋ฅผ ์ง์ ๊ตฌํํด์ค์ผํ๋ค.
์คํฌ๋กค์ด ๋ค ๋์๋์ง ํ์ธ
๋ง์ง๋ง ํ์ด์ง๊น์ง ํ์ด์ง ์๋ฅผ ์ฆ๊ฐ์ํค๋ฉด api๋ฅผ ํธ์ถ
๋ง์ง๋ง ํ์ด์ง๋ผ๋ฉด ๋์ด์ ํ์ด์ง ์๋ฅผ ์ฆ๊ฐ์ํค์ง ์๋ ๋ก์ง
๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋๋ง๋ค ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋ง๋ถ์ฌ์ฃผ๋ ๋ก์ง
๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ ๋ก๋ฉ UI ์ถ๊ฐ
Paging3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด์ฉ ์
DataSource๋ก ๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ
ํ์ด์ง๋ฅผ ์ฆ๊ฐํ๋ ๋ฐฉ๋ฒ
์ ๋ํด์๋ง ์๋ ค์ฃผ๋ฉด ์๋์ผ๋ก ๊ตฌํํด์ผํ๋ ์ ๋ก์ง๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ฒ๋ฆฌํด์ค๋ค!
'๐ค2024 ์๋๋ก์ด๋ > ๐ฟ ์ํ ํ๋ก์ ํธ ๊ฐ๋ฐ ์ผ์ง' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
โบ๏ธAndroid : flow๋ฅผ ์์ฐจ์ ์ผ๋ก ์ฌ์ฉํด๋ณด์~! (2) | 2024.11.04 |
---|---|
ํ๋ก์ ํธ์ Jetpack Navigation ์ ์ฉํ๊ธฐ (8) | 2024.10.15 |
๐จ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์คํฌ๋กค ์ ๋ํ๋ ๊ฒฝ๊ณ (3) | 2024.10.13 |
๐๋ฌดํ ์คํฌ๋กค ์์ ๊ตฌํํด๋ณด๋ค. (5) | 2024.10.12 |
launch๋ ๊ธฐ๋ค๋ ค์ฃผ์ง ์๋๋ค. (async, await , first etc) (8) | 2024.10.11 |