Paging Library 적용기
Paging Library?
- 개념 설명
- Android Jetpack / Architectiure Component에 추가된 라이브러리로 데이터 페이징을 위한 구조를 제공
적용하기
- 예시에서는 ItemKeyedDataSource를 이용하여, LiveData와 연동되는 페이징을 구현하였다.
1. Paging Library 추가
dependencies {
def paging_version = "2.1.0-beta01"
implementation "androidx.paging:paging-runtime:$paging_version"
testImplementation "androidx.paging:paging-common:$paging_version"
implementation "androidx.paging:paging-rxjava2:$paging_version"
}
dependencies {
def paging_version = "1.0.0"
implementation "android.arch.paging:runtime:$paging_version"
testImplementation "android.arch.paging:common:$paging_version"
implementation "android.arch.paging:rxjava2:$paging_version"
}
2. DataSource 선택 / 구현하기
- 데이터를
어디서, 어떻게
가져올 지 정의하는 단계
- 다음의 3가지 인터페이스 중, 페이징 성격에 맞는 하나를 선택한다.
- PageKeyDataSource : 데이터가 다음, 이전 키를 포함하고 있을 때 (ex. nextPageToken 값이 존재할 때 등)
- ItemKeyedDataSource : N번째 데이터로, N-1 / N+1의 데이터를 가져올 때(ex. 날짜별 정렬, 정렬된 ID 등)
- PositionalDataSource : 특정 위치의 데이터를 가져올 때(ex. 100번쨰 위치에서 10개의 데이터를 가져올 때)
- 각 인터페이스의 추상메서드를 구현하는 클래스를 생성한다. (인터페이스별로 추상메서드가 각각 다름)
- PageKeyDataSource
- loadAfter : 추가할(스크롤을 아래로 내릴 때) 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
- loadBefore : 이전(스크롤을 위로 올릴 때) 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
- loadInitial : 최초 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
- ItemKeyedDataSource
- PageKeyDataSource의 3가지 메서드(메서드 이름은 같지만 파라미터는 조금씩 다름)
- getKey : 파라미터로 받은 아이템의 키값 반환
- PositionalDataSource
- loadInitial : 최초 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
- loadRange : 특정 범위의 데이터를 로드하여, 파라미터로 받은 콜백의 onResult 메서드 호출
- 해당 데이터소스를 생성하는 Factory 클래스를 생성한다.
- ex (예제에서는 DateTime의 getMillis()를 키로 사용)
public class DailyDataSource extends ItemKeyedDataSource<Long, DailyItem> {
public static class Factory extends DataSource.Factory<Long, DailyItem> {
@Override
public DataSource<Long, DailyItem> create() {
return new DailyDataSource();
}
}
private DailyDataSource() {}
@Override
public void loadInitial(@NonNull LoadInitialParams<Long> params, @NonNull LoadInitialCallback<DailyItem> callback) {
callback.onResult(fetchAfter(new DateTime(params.requestedInitialKey), params.requestedLoadSize));
}
@Override
public void loadAfter(@NonNull LoadParams<Long> params, @NonNull LoadCallback<DailyItem> callback) {
callback.onResult(fetchAfter(new DateTime(params.key).minusDays(1), params.requestedLoadSize));
}
@Override
public void loadBefore(@NonNull LoadParams<Long> params, @NonNull LoadCallback<DailyItem> callback) {
LocalDate keyDate = new LocalDate(params.key);
if (keyDate.isBefore(LocalDate.now())) {
callback.onResult(fetchBefore(new DateTime(params.key).plusDays(1), params.requestedLoadSize));
}
}
@NonNull
@Override
public Long getKey(@NonNull DailyItem item) {
return item.getDateTime().getMillis();
}
private List<DailyItem> fetchBefore(DateTime key, int howManyDays) {
List<DailyItem> dailyItemList = new ArrayList<>();
return dailyItemList;
}
private List<DailyItem> fetchAfter(DateTime key, int howManyDays) {
List<DailyItem> dailyItemList = new ArrayList<>();
return dailyItemList;
}
}
3. PagedList 생성
- 데이터 갱신을
어떤 방식
으로 관찰할 지 정의하는 단계
- PagedList를 얻기 위해 Builder 클래스가 존재하며, 다음의 2가지 PagedListBuilder 중 맞는 것을 선택한다.
- LivePagedListBuilder : LiveData를 이용하여 데이터를 관찰. 이를 이용하면 LiveData<PagedList< T >> 객체를 받을 수 있다.
- RxPagedListBuilder : RxJava2를 이용하여 데이터 관찰. 이를 이용하면 Flowable<PagedList< T >> / Observable<PagedList< T >> 객체를 받을 수 있다.
- 생성시에, DataSource.Factory 인스턴스를 넘겨주어야 한다.
- 데이터를 불러야 하는 곳에서 build 메서드를 호출한다.
- ex (예제에서는 LivePagedListBuilder 이용)
public class DailyViewModel extends AndroidViewModel {
...
private LivePagedListBuilder<Long, DailyItem> pagedListBuilder;
private DataSource<Long, DailyItem> dataSource;
public DailyViewModel(@NonNull Application application) {
super(application);
PagedList.Config config = new PagedList.Config.Builder()
.setInitialLoadSizeHint(5)
.setPrefetchDistance(4)
.setPageSize(5)
.build();
DataSource.Factory dataSourceFactory = new DailyDataSource.Factory();
pagedListBuilder = new LivePagedListBuilder<>(dataSourceFactory, config);
...
}
public LiveData<PagedList<DailyItem>> load(DateTime key) {
LiveData<PagedList<DailyItem>> dailyList = pagedListBuilder.setInitialLoadKey(key.getMillis()).build();
...
}
public void invalidate() {
dataSource.invalidate();
}
... ViewModel 추가 로직
}
4. PagedListAdapter 구현
- 전달받은 PagedList 객체를 이용하여, 데이터의 변경이 있는지 여부를 확인하여 UI(RecyclerView)를 구성하는 단계
- PagedListAdapter를 상속받는 클래스를 구현해야 한다.
- PagedListAdapter에서 데이터의 변경 여부를 확인하기 위해, 내부적으로
DiffUtil
이 사용되므로 생성자에서 콜백을 구현해서 넘겨주어야 한다..
- PagedListAdapter에서 PagedList를 사용하도록 래핑되어 있다.
- 기존 ReyclerVIew.Adapter의 구현체와 같이, onCreateVIewHolder / onBindViewHolder 메서드를 구현해야 한다.
- ex.
public class DailyAdapter extends PagedListAdapter<DailyItem, DailyViewHolder> {
...
public DailyAdapter() {
super(DIFF_CALLBACK);
}
@NonNull
@Override
public DailyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
...
}
@Override
public void onBindViewHolder(@NonNull DailyViewHolder holder, int position) {
...
}
private static DiffUtil.ItemCallback<DailyItem> DIFF_CALLBACK = new DiffUtil.ItemCallback<DailyItem>() {
@Override
public boolean areItemsTheSame(DailyItem oldItem, DailyItem newItem) {
return oldItem.getDateTime().equals(newItem.getDateTime());
}
@Override
public boolean areContentsTheSame(DailyItem oldItem, DailyItem newItem) {
return oldItem.equals(newItem);
}
};
}
5. 어댑터 연결 & 데이터 바인딩
- RecyclerView에 만들어진 어댑터를 연동하고, PagedListBuilder의 결과를 구독하여, 어댑터에 갱신된 데이터를 넣어준다.
- ex
public class DailyFragment extends BaseFragment {
private DailyViewModel dailyViewModel;
private DailyAdapter adapter;
private RecyclerView recyclerView;
@Override
public View onCreateView(LayoutInflator inflator, ViewGroup container, Bundle savedInstanceState) {
...
initRecyclerView();
initViewModel();
...
}
private void initRecyclerView() {
adapter = new DailyAdapter();
recyclerView.setAdapter(adapter);
...
}
private void initViewModel() {
dailyViewModel = ViewModelProviders.of(this).get(DailyViewModel.class);
dailyViewModel.load(new DateTime()).observe(this, (dailyList) -> {
adapter.submitList(dailyList);
});
}
}
주관적 후기
장점
- 깔끔하게 구조화되어 있어서, 코드가 간결해지고 리팩토링이 쉬워졌다.
- 페이징을 직접 구현하기 위해 고민해야 하는 사항들(ex 스크롤처리, prefetch 등)을 할 필요가 없어져서 핵심 로직에만 집중할 수 있다.
단점
- 모든 라이브러리가 그렇듯이, 라이브러리에 대한 많은 학습이 필요할 것 같다. 생각보다 많은 기능들이 있고, 여러 사용케이스들을 모두 만족하려면, 경험해보지 못한 많은 이슈가 있을 것 같다.
댓글 없음:
댓글 쓰기