[Android] Paging 3.0 Library 알아보기 - 2
Android

[Android] Paging 3.0 Library 알아보기 - 2

728x90

 

 

 

 

 이번 포스팅에는 이전 글에서 언급했었던 Paging2에 없던 Paging3.0만의 기능을 설명해보려고 합니다. Paging3.0의 개념과 기본적인 사용 방법은 이전 글을 참고해주세요.

 

[Android] Paging 3.0 Library 알아보기

Paging이란?  페이징이란 데이터를 가져올 때 한 번에 모든 데이터를 가져오는 것이 아니라 일정한 덩어리로 나눠서 가져오는 것을 뜻합니다. 예를 들어, 구글에서 어떤 키워드로 검색하게 되면

leveloper.tistory.com

 

 

 

 

1. 데이터 스트림 변환

 Paging을 사용할 때 데이터를 가공해야 하는 경우가 많이 생깁니다. 예를 들어, 데이터를 화면에 표시하기 전에 먼저 필터링을 하거나 데이터를 다른 형태로 변환해야 할 수도 있습니다. Paging3.0은 기본적으로 Flow를 지원하기 때문에 이와 같은 데이터 변환이 간편합니다.

 아래의 코드는 지난 글에서 사용했던 Paging3.0 샘플 코드입니다.

@HiltViewModel
class PagingViewModel @Inject constructor(
    private val repository: PagingRepository
) : ViewModel() {

    val pagingData = repository.getPagingData().map { pagingData ->
        pagingData.map<String, SampleModel> { SampleModel.Data(it) }
            .insertHeaderItem(item = SampleModel.Header("Header"))
            .insertFooterItem(item = SampleModel.Header("Footer"))
            .insertSeparators { before: SampleModel?, after: SampleModel? ->
                if (before is SampleModel.Header || after is SampleModel.Header)
                    SampleModel.Separator
                else
                    null
            }
    }.cachedIn(viewModelScope)
}

sealed class SampleModel(val type: SampleType) {
    data class Data(val value: String): SampleModel(SampleType.DATA)
    data class Header(val title: String): SampleModel(SampleType.HEADER)
    object Separator: SampleModel(SampleType.SEPARATOR)
}

enum class SampleType {
    HEADER, DATA, SEPARATOR
}

 PagingData.map() 함수로 다른 데이터로 변환이 가능합니다. map() 뿐 아니라 filter(), flatMap()과 같은 함수도 지원합니다.

 Paging3.0에서는 어댑터에 연결해주기 전에 Header와 Footer, Separator를 추가해주는 것이 간편해졌습니다. insertHeaderItem(), insertFooterItem(), insertSeparators() 함수로 페이징 데이터 중간중간에 다른 데이터를 넣는 것이 가능합니다. 개인적으로 Paging2를 사용할 때는 헤더 및 푸터를 넣는 게 쉽지 않았는데 굉장히 마음에 드는 기능이라고 생각합니다.

 

 

 

 

2. refresh(), retry()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: PagingViewModel by viewModels()

    private val adapter: PagingAdapter by lazy { PagingAdapter() }
    private val recyclerView: RecyclerView by lazy { findViewById(R.id.recyclerView) }
    private val swipeRefreshLayout: SwipeRefreshLayout by lazy { findViewById(R.id.swipeRefreshLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            viewModel.pagingData.collectLatest {
                adapter.submitData(it)
            }
        }

        swipeRefreshLayout.setOnRefreshListener {
            // refresh
            adapter.refresh()
            swipeRefreshLayout.isRefreshing = false
        }
    }
}

 PagingDataAdapter에는 refresh()와 retry() 함수가 생겼습니다. refresh() 함수는 호출 시 처음부터 데이터를 다시 로드하게 됩니다.

 retry() 함수는 일반적인 상황에서는 아무런 동작을 하지 않습니다. retry라는 이름에 맞게 PagingSource에서 LoadResult.Error를 리턴한 경우에 에러가 발생한 페이지부터 다시 로드하게 됩니다. retry() 함수는 아래에서 좀 더 살펴보겠습니다.

 

 

3. LoadStateAdapter

 네트워크 연결이 불안정할 때 Paging에서 데이터를 로드하려고 하면 딜레이가 발생할 수 있습니다. Paging3.0에 새롭게 추가된 LoadStateAdapter는 Loading이나 Error 처리를 간편하게 할 수 있도록 도와줍니다.

 

 LoadStateAdapter는 RecyclerView.Adapter를 상속받은 클래스이며, 구현해줘야 하는 함수는 다음과 같습니다.

abstract fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): VH

abstract fun onBindViewHolder(holder: VH, loadState: LoadState)

 

 얼핏 보면 RecyclerView.Adapter의 onCreateViewHolder, onBindViewHolder와 같아 보이지만, 받는 인자가 다릅니다. 인자로 받는 LoadState도 Paging3.0에서 새롭게 추가된 클래스로써, Loading, NotLoading, Error 세 가지로 나뉩니다.

public sealed class LoadState(
    public val endOfPaginationReached: Boolean
) {

    public class NotLoading(
        endOfPaginationReached: Boolean
    ) : LoadState(endOfPaginationReached) {
        ...
    }

    public object Loading : LoadState(false) {
        ...
    }

    public class Error(
        public val error: Throwable
    ) : LoadState(false) {
        ...
    }
}

 endOfPaginationReached 값은 로딩 가능 여부를 나타냅니다. 해당 값이 true면 더 이상 로딩을 못하는 상태입니다.

 

 

class PagingLoadStateAdapter(
    private val retry: () -> Unit
) : LoadStateAdapter<PagingLoadStateViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): PagingLoadStateViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return PagingLoadStateViewHolder(ItemLoadStateBinding.inflate(layoutInflater, parent, false), retry)
    }

    override fun onBindViewHolder(holder: PagingLoadStateViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }
}

class PagingLoadStateViewHolder(
    private val binding: ItemLoadStateBinding,
    private val retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(state: LoadState) {
        binding.retryButton.setOnClickListener { retry() }
        binding.isLoading = state is LoadState.Loading
        binding.isError = state is LoadState.Error
        binding.errorMessage = (state as? LoadState.Error)?.error?.message ?: ""
    }
}

 LoadStateAdapter를 구현해줍니다. 어댑터의 인자로 받은 retry() 함수는 위에서 설명한 PagingDataAdapter의 retry() 함수를 뜻합니다. 에러가 발생했을 때 버튼 클릭 시 에러가 난 페이지부터 새로 데이터를 호출해주기 위해 연결해줍니다.

 

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: PagingViewModel by viewModels()

    private val adapter: PagingAdapter by lazy { PagingAdapter() }
    private val recyclerView: RecyclerView by lazy { findViewById(R.id.recyclerView) }
    private val swipeRefreshLayout: SwipeRefreshLayout by lazy { findViewById(R.id.swipeRefreshLayout) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView.adapter = adapter.withLoadStateHeaderAndFooter(
            PagingLoadStateAdapter { adapter.retry() },
            PagingLoadStateAdapter { adapter.retry() }
        )

        lifecycleScope.launch {
            viewModel.pagingData.collectLatest {
                adapter.submitData(it)
            }
        }

        swipeRefreshLayout.setOnRefreshListener {
            // refresh
            adapter.refresh()
            swipeRefreshLayout.isRefreshing = false
        }
    }
}

 LoadStateAdapter를 만들었으면 PagingDataAdapter에 연결해줘야 합니다. withLoadStateFooter, withLoadStateHeader, withLoadStateHeaderAndFooter 함수로 연결해줄 수 있으며, 함수 결과로 ConcatAdapter를 반환합니다.

 

 

 네트워크에서 지연이 발생한 것을 테스트해주기 위해 지난 글에서 작성했던 PagingSource의 load() 함수에서 delay(500)을 걸어주겠습니다.

class SamplePagingSource @Inject constructor(
    private val service: SampleBackendService
) : PagingSource<Int, String>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        return try {
            // delay 발생
            delay(5000)

            ...
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, String>): Int? {
        ...
    }
}

 

 

 

 에러 발생도 테스트해주기 위해 간헐적으로 익셉션을 발생시켜 줍니다.

class SamplePagingSource @Inject constructor(
    private val service: SampleBackendService
) : PagingSource<Int, String>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        return try {
            delay(500)

            // 에러 발생 !
            if (Random.nextFloat() < 0.5) {
                throw Exception("error !!!")
            }

            ...
            
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, String>): Int? {
        ...
    }
}

 

 

 

 

 

예제 소스

https://github.com/tkdgusl94/blog-source/tree/master/paging3

 

tkdgusl94/blog-source

https://leveloper.tistory.com/ 에서 제공하는 예제 source. Contribute to tkdgusl94/blog-source development by creating an account on GitHub.

github.com

 

참고

https://two22.tistory.com/5

 

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

Paging 라이브러리란? 페이징을 쉽게 구현하기 위해 안드로이드에서 제공하는 라이브러리이다. 최근 3.0 Alpha 버전이 릴리즈 되었단 걸 알게 되었다. Paging2와 변한 점 DataSource 관련 코드가 PagingSource

two22.tistory.com

https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ko 

 

네트워크 및 데이터베이스의 페이지  |  Android 개발자  |  Android Developers

네트워크 연결이 불안정하거나 사용자가 오프라인 상태일 때 앱을 사용할 수 있도록 하여 사용자 경험을 향상하세요. 이를 위한 한 가지 방법은 네트워크와 로컬 데이터베이스에서 동시에 페이

developer.android.com

 

728x90