본문 바로가기

안드로이드/Compose Paging3

[안드로이드 Jetpack Compose Paging3 시리즈] PagingConfig, PagingSource 적용하기 #3


1. Paging 데이터 스트림 구현

안녕하세요. 이번 포스팅에서는 안드로이드 Jetpack Compose Paging3 라이브러리의 PagingConfig와 PagingSource를 구현해서 페이징 데이터 스트림을 구현하겠습니다. MovieApi Interface에 영화 리스트를 불러오는 함수를 정의한 후, MovieRepository에 PagingConfig와 PagingSource를 적용해서 Pager 객체를 만들겠습니다. DB 캐싱은 제외한 페이징을 구현할 것이기 때문에 RemoteMediator는 구현하지 않아도 됩니다. 

페이징 다이어그램
페이징 다이어그램

1-1. MovieApi 및 MovieRemoteDataSource에 getMovies 함수 정의

네트워크 통신을 통해 영화 목록을 불러오기 위해 retrofit interface에 getMovies() 함수를 정의하고, 이를 불러오는 RemoteDataSource 영역에서도 getMovies 함수를 정의 및 구현하겠습니다.

interface MovieApi {
    @GET("/movies?&_sort=category,id")
    suspend fun getMovies(
        @Query("_page") page: Int,
        @Query("_limit") limit: Int,
    ): List<MovieData>
}
interface MovieDataSource {
    interface Remote {
        suspend fun getMovies(page: Int, limit: Int): ApiResult<List<MovieEntity>>
    }
}

class MovieRemoteDataSource(
    private val movieApi: MovieApi
) : MovieDataSource.Remote {
    override suspend fun getMovies(page: Int, limit: Int): ApiResult<List<MovieEntity>> = try {
        val result = movieApi.getMovies(
            page = page,
            limit = limit
        )
        ApiResult.Success(result.map { it.toDomain() })
    } catch (e: Exception) {
        ApiResult.Error(e)
    }
}

1-2. MovieRepository에서 Pager 생성

MovieRepository에 Pager 객체를 만들어서 데이터 스트림을 반환해야 합니다. 이전 포스팅에서 설명한 것처럼 Pager에 PagingConfig 및 PagingSource를 전달해 줍니다. 이번에는 DB 캐싱 처리는 하지 않기 때문에 RemoteMediator는 구현하지 않아도 됩니다.  

class MovieRepositoryImpl(private val remote: MovieDataSource.Remote) : MovieRepository {
    override fun movies(pageSize: Int): Flow<PagingData<MovieEntity>> {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = false,
            ),
            pagingSourceFactory = { MoviesPagingSource(remote) }
        ).flow
    }
}

class MoviesPagingSource(
    private val remote: MovieDataSource.Remote
) : PagingSource<Int, MovieEntity>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MovieEntity> {
        val page = params.key ?: 1
        return remote.getMovies(page, params.loadSize).getResult({
            LoadResult.Page(
                data = it.data,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (it.data.isEmpty()) null else page + 1
            )
        }, {
            LoadResult.Error(it.error)
        })
    }
    override fun getRefreshKey(state: PagingState<Int, MovieEntity>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey
        }
    }
}

PagingSource 추상클래스를 상속받아서 MoviesPagingSource 클래스를 pagingSourceFactory로 전달하고 있습니다. load 및 getRefreshKey 함수를 재정의 해야 하는데, load 함수에서는 LoadResult.Page 또는 LoadResult.Error를 반환해야 합니다. getRefreshKey()는 refresh시 다시 시작할 key를 반환해 주면 됩니다.

1-3. UseCase를 만들고 UI Layer에서 데이터 스트림 구독

저는 Domain Layer를 구현했기 때문에 MovieRepository의 접근을 UseCase에서 하고 있습니다. 그래서 영화 목록 데이터 스트림을 반환하는 GetMoviesUseCase를 만들고, 이를 ViewModel에서 불러와서 데이터 스트림을 받도록 구현했습니다.

class GetMovies (
    private val movieRepository: MovieRepository
){
    operator fun invoke(pageSize: Int): Flow<PagingData<MovieEntity>> = movieRepository.movies(pageSize)
}

@HiltViewModel
class MovieListViewModel @Inject constructor(
    private val getMovies: GetMovies,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    init {
        viewModelScope.launch {
            getMovies(20).cachedIn(viewModelScope).collectLatest { pagingData ->
                
            }
        }
    }
}

2. PagingConfig, PagingSource, 데이터스트림 구독 정리

이번 시간에는 안드로이드 Jetpack Compose Paging3 라이브러리의 Pager 객체를 만들고 데이터 스트림을 ViewModel에서 구독하는 기능까지 구현했습니다. Pager 객체를 만들기 위해서 PagingConfig와 PagingSource를 구현하였고, PagingSource의 load() 함수에서는 네트워크 통신을 통해 페이징 데이터를 가져올 수 있도록 MovieApi Interface에 정의한 getMovies() 함수를 호출했습니다.

ViewModel에서는 데이터 스트림을 구독해서 pagingData를 받아올 수 있게 되었습니다. xml기반으로 작업을 한다면 RecyclerView의 Adapter에 submit 함수에 pagingData를 전달하게 되며, Jetpack Compose로 작업을 한다면 Adapter 없이 LazyColumn을 통해 구현을 하게 됩니다.

다음 포스팅에서는 Jetpack Compose를 통해 pagingData를 화면에 보여주는 방법에 대해 설명하겠습니다.

반응형