//MyPagingSource.kt
private const val GITHUB_STARTING_PAGE_INDEX = 1
// PagingSource - 데이터 소스를 정의하고 이 소스에서 데이터를 가져오는 방법을 정의
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() { //인덱스를 구분하는 값(보통 1,2,3페이지 등등 int) , 로드할 아이템의 유형
//load 함수는 사용자가 스크롤 할 때마다 데이터를 비동기적으로 가져온다.
//loadParams : 로드 작업과 관련된 정보
//LoadResult : 로드 결과 (LoadResult.Page: 로드에 성공한 경우 . 데이터와 이전 다음 페이지 Key가 포함 / LoadResult.Error: 오류가 발생한 경우)
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX //로드할 페이지의 키==현재 페이지 인덱스.
// 로드가 처음 호출되는 경우에는 LoadParams.key가 null입니다. 그렇기 때문에 (1페이지 부터 로드하고 싶다면) 1 을 넣어줍니다.
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(
apiQuery,
position,
params.loadSize
) //params.loadSize - 가져올 데이터의 갯수를 관리한다.
//API에 따라 limit(한 번에 보여줄 데이터의 수 - params.loadsize), offset==cursor(데이터의 인덱스 - position) 등으로 페이징 처리 되어 있습니다.
val repos = response.items
val nextKey = if (repos.isEmpty()) {
null //네트워크 응답에 성공했지만 목록이 비어 있는 경우에는 로드할 데이터가 없는 것으로 간주할 수 있습니다. 따라서 nextKey가 null일 수 있습니다.
} else {
// initial load size = 3 * NETWORK_PAGE_SIZE
// ensure we're not requesting duplicating items, at the 2nd request
position + (params.loadSize / NETWORK_PAGE_SIZE)
}
//리턴값
LoadResult.Page(
data = repos, //로드된 데이터
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
//스와이프 Refresh나 데이터 업데이트 등으로 현재 목록을 대체할 새 데이터를 로드할 때 사용된다.
//PagingData는 Component에서 설명한 것처럼 새로고침 될 때마다 상응하는 PagingData를 생성해야한다.
//즉, 수정이 불가능하고 새로운 인스턴스를 만들어야한다.
//가장 최근에 접근한 인덱스인 anchorPosition으로 주변 데이터를 다시 로드한다.
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
//MyRepository.kt
class MyRepository(private val service: GithubService) {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {//flow : 코루틴에서 리액티브 프로그래밍(데이터가 변경 될 때 이벤트를 발생시켜 데이터를 계속해서 전달하도록 하는 방식)을 구현하기 위해 사용합니다.
return Pager(
//PagingConfig 클래스는 로드 대기 시간, 초기 로드의 크기 요청 등 PagingSource에서 콘텐츠를 로드하는 방법에 관한 옵션을 설정합니다.
//정의해야 하는 유일한 필수 매개변수는 각 페이지에 로드해야 하는 항목 수를 가리키는 페이지 크기입니다.
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE, //각 페이지에 로드해야 하는 항목 수
//PagingConfig.pageSize는 여러 화면의 항목이 포함될 만큼 충분히 커야 합니다. 페이지가 너무 작으면 페이지의 콘텐츠가 전체 화면을 가리지 않기 때문에 목록이 깜박일 수 있습니다.
//페이지 크기가 클수록 로드 효율이 좋지만 목록이 업데이트될 때 지연 시간이 늘어날 수 있습니다.
enablePlaceholders = false // 플레이스 홀더 사용 여부 사용하면 아직 로드되지 않은 데이터에 대해 null 값이 들어간 뷰가 만들어 진다.
),
pagingSourceFactory = { GithubPagingSource(service, query) } //pagingSource 인스턴스 생성
).flow
}
companion object {
const val NETWORK_PAGE_SIZE = 50
}
}
//SearchRepositoriesViewModel.kt
class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() {
fun searchRepo(queryString: String): Flow<PagingData<Repo>> {
val newResult: Flow<PagingData<Repo>> = repository.getSearchResultStream(queryString)
.cachedIn(viewModelScope) //cachedIn(viewModelScope)를 사용하여 캐싱을 해줄 수 있습니다.
return newResult
}
}
//ReposAdapter.kt
// PagingData 콘텐츠가 로드될 때마다 PagingDataAdapter에서 알림을 받은 다음 RecyclerView에 업데이트하라는 신호를 보냅니다.
class ReposAdapter : PagingDataAdapter<Repo, RepoViewHolder>(DIFF_CALLBACK) {
// body는 recyclerviewAdapter처럼 사용하면 된다.
}
//MyActivity.kt
private var searchJob: Job? = null
private fun search(query: String) {
//Flow<PagingData>를 사용하려면 새 코루틴을 실행해야 합니다. 이 작업은 활동이 다시 생성될 때 요청을 취소하는 lifecycleScope에서 실행됩니다.
// Make sure we cancel the previous job before creating a new one
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModel.searchRepo(query).collectLatest {
adapter.submitData(it)
}
}
}