2018년 11월 27일 화요일

[안드로이드] Paging Library 적용기

Paging Library 적용기

Paging Library?

  • 개념 설명
  • Android Jetpack / Architectiure Component에 추가된 라이브러리로 데이터 페이징을 위한 구조를 제공

적용하기

  • 예시에서는 ItemKeyedDataSource를 이용하여, LiveData와 연동되는 페이징을 구현하였다.

1. Paging Library 추가

  • AndroidX
dependencies {
    def paging_version = "2.1.0-beta01"

    implementation "androidx.paging:paging-runtime:$paging_version" // use -ktx for Kotlin

    // alternatively - without Android dependencies for testing
    testImplementation "androidx.paging:paging-common:$paging_version" // use -ktx for Kotlin

    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version" // use -ktx for Kotlin
}
  • AndroidX 이전
dependencies {
    def paging_version = "1.0.0"

    implementation "android.arch.paging:runtime:$paging_version"

    // alternatively - without Android dependencies for testing
    testImplementation "android.arch.paging:common:$paging_version"

    // optional - RxJava support
    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);
        ...
    }
    // 특정 위치(key)로 이동시에 호출
    public LiveData<PagedList<DailyItem>> load(DateTime key) {
        LiveData<PagedList<DailyItem>> dailyList = pagedListBuilder.setInitialLoadKey(key.getMillis()).build();
        ...
    }
    
    // 현재 DataSource가 갱신되었을 때 호출
    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) -> {
            // 얻은 PagedList를 어댑터에 전달
            // submitList() 메서드는 PagedListAdapter에 구현되어 있는 메서드로, 내부적으로 화면갱신을 해주기 때문에 notifyXXX 메서드를 호출하지 않아도 된다.
            adapter.submitList(dailyList);
        });
    }
}

주관적 후기

장점

  • 깔끔하게 구조화되어 있어서, 코드가 간결해지고 리팩토링이 쉬워졌다.
  • 페이징을 직접 구현하기 위해 고민해야 하는 사항들(ex 스크롤처리, prefetch 등)을 할 필요가 없어져서 핵심 로직에만 집중할 수 있다.

단점

  • 모든 라이브러리가 그렇듯이, 라이브러리에 대한 많은 학습이 필요할 것 같다. 생각보다 많은 기능들이 있고, 여러 사용케이스들을 모두 만족하려면, 경험해보지 못한 많은 이슈가 있을 것 같다.

댓글 없음:

댓글 쓰기