๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Dev/Android

[Android] Jetpack Paging 3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉํ•˜๊ธฐ

by JUNE.C 2021. 6. 26.

๐Ÿง 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 Library Example in App Architecture (์ถœ์ฒ˜: Android Developer Paging3)

 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์œผ๋กœ ์ฃผ๋ณ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•œ๋‹ค.

 

๐Ÿ“Œ 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

Android Codelabs - Paging


 RemoteMediator, LoadState, Header, Footer ๋“ฑ์€ ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

 ์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค. ๊นƒํ—ˆ๋ธŒ ๋ฐ”๋กœ๊ฐ€๊ธฐ

 

 

๋Œ“๊ธ€