본문 바로가기

안드로이드

안드로이드 Paging 3.0 #1 - 맛보기

반응형

Paging 라이브러리란?

페이징을 쉽게 구현하기 위해 안드로이드에서 제공하는 라이브러리이다.
최근 3.0 Alpha 버전이 릴리즈 되었단 걸 알게 되었다.

Paging2와 변한 점

  1. DataSource 관련 코드가 PagingSource 하나로 통합되었다.
  2. Header, Footer, separator를 넣을 수 있는 코드 (insertHeaderItem, insertFooterItem, insertSeparators)가 추가되었다.
  3. 로딩 처리를 쉽게 할 수 있다.
  4. 데이터 refresh 에 대한 처리가 추가되었다.
  5. 로딩 시 발생하는 에러 처리를 쉽게 할 수 있다.
  6. 더 이상 Config 생성 방식이 빌더 패턴이 아니다.
  7. 데이터의 캐싱이 가능하다.

위에 7가지 정도가 바뀌었다.

기존 Paging 보단 코드가 훨씬 깔끔하게 나오는 걸 느꼈다.

Paging3 Library 시작하기

이제 위와 같은 간단한 샘플을 만들어보려 한다.

라이브러리 추가하기

dependencies {
  def paging_version = "3.0.0-alpha01"

  implementation "androidx.paging:paging-runtime:$paging_version"

  // testImplementation "androidx.paging:paging-common:$paging_version" // test 용 
  // implementation "androidx.paging:paging-rxjava2:$paging_version"    // rxJava2 support
  // implementation "androidx.paging:paging-guava:$paging_version"      // guava support
}

Paging Source 만들기

class PagingRepository {
    fun getPagingData(page: Int): Pair<List<String>, Int?> {
        return if (page <= 30) Pair(listOf("A $page", "B $page", "C $page"), page + 1)
        else Pair(listOf(), null)
    }
}

우선 테스트용 Repository를 만들었다.
데이터는 30 Page까지만 존재하고 각 페이지에는 [ A, B, C ] 3개의 문자열만 가지고 있다.

class DooolPagingSource(
        private val repository: PagingRepository
) : PagingSource<Int, String>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        return try {
            val nextPage = params.key ?: 1
            val response = repository.getPagingData(nextPage)

            LoadResult.Page(
                    data = response.first,
                    prevKey = null,
                    nextKey = response.second
            )
        } catch (e: Exception) {
            LoadResult.Error(Throwable("Paging Error"))
        }
    }
}

추상 클래스인 PagingSource<Key : Any, Value : Any> 를 상속받아서 DooolPagingSource를 만들었다.

 

인자로 넘어오는 params의 key 값이 페이지 정보이다. 추가로 loadSize ( 한 번에 불러올 사이즈 ) 도 포함하고 있다.

 

return 값인 LoadResult는 Page, Error 두 가지가 있다.

LoadResult.Page 는 정상적인 흐름일 때 사용하면 된다.
prevKey, nextKey를 둘 다 null로 주면 페이징은 더 이상 데이터를 불러오지 않는다.

 

LoadResult.Error 는 Exception 발생이나 데이터에 문제가 있을 경우 사용하면 된다.
return 할 시에는 자동 리프레시 같은 건 없다. 그냥 끝이다.

데이터 모델 정의하기

enum class DataType {
    HEADER, ITEM, SEPARATOR
}

sealed class DataModel(val type: DataType) {
    data class Item(val title: String) : DataModel(DataType.ITEM)
    data class Header(val title: String) : DataModel(DataType.HEADER)
    object Separator : DataModel(DataType.SEPARATOR)
}

Adapter에서 사용할 데이터를 Item, Header, Separator 세 가지로 정의하였다.

PagingData 만들기

class PagingViewModel : ViewModel() {

    ...

    val flow = Pager(PagingConfig(pageSize = 10)) { // config 설정
        DooolPagingSource(PagingRepository())       // pagingSource 연결
    }.flow.map {
        it.map<DataModel> { DataModel.Item(it) }   
                .insertHeaderItem(DataModel.Header("HEADER"))
                .insertFooterItem(DataModel.Header("FOOTER"))
                .insertSeparators { before, after ->
                    when{
                        before is DataModel.Item && after is DataModel.Item ->{
                            if (before.title.startsWith("C") && after.title.startsWith("A")) DataModel.Separator
                            else null
                        }
                        else -> null
                    }
                }
    }.cachedIn(viewModelScope)                      // 캐싱

    ...

}

PagingSource에서 던져주는 건 그냥 아이템뿐이기에 Pager 를 통해서 PagingData로 변환해줘야 한다.

위에 소스에는 없지만 Pager 생성 시에 초기 키값을 지정해 줄 수 도 있다.

 

Pager 생성 후 flow 변수 ( 왜 이름을 이렇게... )를 통해서 PagingData를 가져올 수 있다.

 

PagingData는 insertHeaderItem, insertFooterItem, insertSeparators 함수를 통해서
헤더, 푸터, 사이사이 아이템을 넣는 게 가능하다.

 

위에 코드에선 헤더와 푸터에 Header 아이템을 넣고, 페이지와 페이지 사이에 Separator 아이템을 넣었다.
( 페이지의 시작에 A , 끝에 C 문자열이 온다. )

insertSeparators의 before 또는 after 값에는 앞에서 실행한 insert코드의 아이템이 올 수 있으니 주의해야 한다.

 

캐싱은 cachedIn(viewModelScope) 코드를 사용하면 가능하다. ( cachedIn 은 Flow 타입에서 지원하는 함수이다. )

액티비티에서 한번 불러오면 프래그먼트들이 바로 아이템을 사용하는 것이 가능하다.

PagingDataAdapter 구현하기

class PagingAdapter(diffCallback: DiffUtil.ItemCallback<DataModel>) :
        PagingDataAdapter<DataModel, RecyclerView.ViewHolder>(diffCallback) {

    ...

}

PagingDataAdapter 는 기존 RecyclerView.Adapter의 구현과 동일하다.
차이점은 diffUtil을 구현해줘야 하는 것뿐이다. ( paging2와 동일하다. )

데이터 연결하기

class MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<PagingViewModel>()

    ...

    override fun onCreate(savedInstanceState: Bundle?) {

        ...

        recycler_view.layoutManager = LinearLayoutManager(this)
        recycler_view.adapter = adapter

        lifecycleScope.launch {
            viewModel.flow.collectLatest { pagingData ->
                adapter.submitData(pagingData)
            }
        }
    }
}

submitDataflow 의 함수가 전부 suspend 함수라서 코루틴을 통하여 호출하여야 한다.

데이터 다시 불러오기

adapter.refresh()
adapter.retry()

refresh 는 언제든지 호출하여 처음부터 다시 불러오는 것이 가능하다.

retry 는 일반적인 상황에선 아무런 동작도 하지 않지만, PagingSource에서 LoadResult.Error 를 리턴한 경우에는 에러가 발생한 해당 페이지부터 다시 불러오기 시작한다.

 

code : github.com/D000L/paging3_sample

반응형