๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿค2024 ์•ˆ๋“œ๋กœ์ด๋“œ/๐Ÿฟ ์˜ํ™” ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœ ์ผ์ง€

๋ฌดํ•œ์Šคํฌ๋กค์— Paging3 ์ ์šฉํ•˜๊ธฐ

by hyeonha 2024. 10. 13.

๋ชฉ์ฐจ

    ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—

    ์ˆ˜๋™์œผ๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ Paging3์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์€ ์–ด๋–ค ๊ฒƒ์ผ๊นŒ๋ฅผ ์ฐพ์•„๋ณด์•˜๋‹ค.

    ๊ทธ๋žฌ๋”๋‹ˆ ๋‚ด๊ฐ€ ์ˆ˜๋™์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋˜ ๊ฑฐ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๊ณ  ์žˆ์—ˆ๋‹ค!!!! 

    1. ์Šคํฌ๋กคํ•˜๋ฉด์„œ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ํ˜ธ์ถœ

    2. ์ƒˆ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ์— ๋ณด์—ฌ์ฃผ๊ธฐ

    3. ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์— ์˜ค๋ฉด ์Šคํƒ‘

    4. ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ถ”๊ฐ€

    ๋“ฑ์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ชจ๋‘ ์ œ๊ณตํ•ด์ฃผ์—ˆ๋‹ค.

    ์—„์ฒญ๋‚˜๋‹น

     

    ์•ž์œผ๋กœ๋„ ์‚ฌ์šฉ์ž ํŽ˜์ด์ง€๋‚˜ ํŒ”๋กœ์ž‰ ํ™”๋ฉด ๋“ฑ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•ด์•ผํ•  ๋•Œ๊ฐ€ ๋งŽ์•˜๊ณ  ์•ฑ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง์— ๋”ฐ๋ผ ์ˆ˜๋™์œผ๋กœ ๋ชจ๋‘ ๊ตฌํ˜„ํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๋ฉด ํšจ์œจ์ ์ด๊ณ  ์˜ค๋ฅ˜ ๋ฐœ์ƒ๋„ ์ค„์ผ ์ˆ˜ ์žˆ์„๊ฑฐ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. 

     

    ๋”ฐ๋ผ์„œ  ๊ธฐ์กด ์ˆ˜๋™์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋˜ ๊ฒ€์ƒ‰ ์‹œ ๊ด€๋ จ ์˜ํ™” ํฌ์Šคํ„ฐ๋ฅผ ํ‘œ์‹œํ•ด์ฃผ๋˜ ๋ถ€๋ถ„์— Paging3๋ฅผ ์ ์šฉํ•ด๋ณด์•˜๋‹ค. 

     

    ์ฒ˜์Œ Paging3๋ฅผ ์ด์šฉํ•˜๋ ค๊ณ  ํ–ˆ์„ ๋•Œ์—๋Š” ๊ฐœ๋…์ด ์กฐ๊ธˆ ์–ด๋ ค์›Œ ์ดํ•ดํ•˜๋Š”๋ฐ ์‹œ๊ฐ„์ด ๋‹ค์†Œ ๊ฑธ๋ ธ๋‹ค.

    ๋”ฐ๋ผ์„œ ์ดํ•ดํ–ˆ๋˜ ๊ฐœ๋…๋ถ€ํ„ฐ ์ •๋ฆฌํ•˜๊ณ  ๋„˜์–ด๊ฐ€๋ณด๋ ค ํ•œ๋‹น


    Paging3 ์‚ดํŽด๋ณด๊ธฐ

    Paging ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” 3๊ฐ€์ง€ ๋ ˆ์ด์–ด์—์„œ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ๊ตฌํ˜„ํ•ด์ค˜์•ผํ•œ๋‹ค.

    ์ถœ์ฒ˜ : Android ๊ณต์‹ ๋ฌธ์„œ

    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๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•

    ํŽ˜์ด์ง€๋ฅผ ์ฆ๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•

    ์— ๋Œ€ํ•ด์„œ๋งŒ ์•Œ๋ ค์ฃผ๋ฉด ์ˆ˜๋™์œผ๋กœ ๊ตฌํ˜„ํ•ด์•ผํ–ˆ๋˜ ์œ„ ๋กœ์ง๋“ค์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค!

    728x90