레이블이 Architecture Component인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Architecture Component인 게시물을 표시합니다. 모든 게시물 표시

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 등)을 할 필요가 없어져서 핵심 로직에만 집중할 수 있다.

단점

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

2018년 6월 5일 화요일

[안드로이드] Android JetPack

Use Android Jetpack to Accelerate Your App Development

이 포스트에서, 안드로이드 앱 개발을 더 쉽도록 도와주는 차세대의 컴포넌트, 도구, 구조적 가이드라인을 포함하는 JetPack을 소개하려고 한다.
JetPack은 새로운 안드로이드의 기능들이 하위호환성을 유지할 수 있도록 도와주는 Support Library로부터 고안되었다. 그리고, 앱의 라이프사이클과 관련된 데이터 처리, 수시로 변하는 데이터 처리의 복잡성을 쉽게 처리할 수 있도록 디자인된 아키텍쳐 컴포넌트를 소개했다. 아키텍쳐 컴포넌트를 작년에 발표한 이래로 많은 앱들이 이 컴포넌트를 사용하기 시작했다. LinkedIn, Zillow, iHeartRadio 같은 회사들은 더 적은 버그, 더 쉬워진 테스트, 앱의 순수 기능에만 집중할 수 있는 시간 등의 이점을 보았다.

What is Android JetPack

Android JetPack은 좋은 앱을 만들기 위한 컴포넌트, 도구, 가이드라인의 모음 이다. JetPack은 기존의 서포트 라이브러리, 아키텍쳐 컴포넌트들을 모두 가져와서 4개의 카테고리로 재배치했다.


JetPack 라이브러리는 안드로이브 플랫폼의 일부로 포함되지 않고(unbundled) 별개로 제공되는 라이브러리 이다. 즉, 각 컴포넌트를 필요한 시점에 맞추어 적욧할 수 있다는 의미다. JetPack에 새로운 기능이 추가되면 손쉽게 앱에 반영하여 배포할 수 있다. JetPack으로 변경되면서,기존/신규 컴포넌트들이 androidx.*로 이동되었다. 해당 내용은 다음의 포스트를 참고한다.
또한, 서포트 라이브러리의 취지대로, JetPack의 컴포넌트도 특정 버전에 종속되지 않도록 만들어졌기 때문에 다양한 안드로이드 버전의 플랫폼에서 실행할 수 있다.
게다가, JetPack은 관심의 분리 / 테스트 가능을 중점적으로 하는 현대 설계기법과 코틀린 통합과 같은 생산성 관련 내용들도 고려하여 제작되었다. 이는 더 적은 코드로 견고하고 고품질의 앱을 쉽게 만들도록 도와준다. JetPack의 컴포넌트들은 유기적으로 동작할 수 있도록 구현되었지만, 개발자는 굳이 모두를 사용할 필요 없이 필요한 부분만 사용할 수도 있다. 그러므로 이미 구현된 모든 부분을 고칠 필요 없이, 필요한 부분에만 JetPack 컴포넌트를 순차적으로 교체할 수도 있다.
우리는 다음과 같은 피드백을 받았기 때문에, JetPack의 이러한 특성이 얼마나 중요한지 알고 있다.
우리 앱은 MVVM으로 코드 구조 변경을 고려하고 있었습니다. 아키텍쳐 컴포넌트는 이 고민을 쉽게 해결하고 구현할 수 있도록 도와주었습니다. 그리고, 테스트하기도 훨씬 수월해졌습니다. ViewModel만 유닛테스트를 수행하면 되기 때문에 코드가 전보다 훨씬 견고해졌습니다.

What's New

다음의 5가지 새로운 컴포넌트가 JetPack에 곧 추가될 것이다.
  • WorkManager alpha release
  • Navigation alpha release
  • Paging stable release
  • Slices alpha release
  • Android KTX (Kotlin Extensions) alpha release

WorkManager

WorkManager 컴포넌트는 백그라운드 제약상황에서 실행이 보장되어야 하는 백그라운드 동작을 구현하기 위한 멋진 해결방법을 제공하는 라이브러리 이다. 이 라이브러리는 다음과 같은 특징을 가지고 있다.
  • 단순함
  • 최근 스타일의 API
  • Google Play Service의 유무에 상관없이 동작할 수 있음
  • work의 그래프를 생성하여 순서를 지정할 수 있음
  • work의 상태를 조회할 수 있음
    자세한 내용은 공식문서에서 확인할 수 있다.

Navigation

엑티비티는 시스템에서 제공하는 앱 UI의 진입점이지만, 화면간에 데이터를 공유할떄나, 화면전환에서 유연성이 떨어지는 점 때문에 이상적인 아키텍쳐라고 볼 수 없었다. 그래서 하나의 엑티비티만을 사용하는 것을 기본 아키텍쳐로 하여 앱을 만들도록 하는 Navagation 컴포넌트가 도입되었다. 프레그먼트의 지원을 통해 Lifecycle, ViewModel 같은 아키텍쳐 컴포넌트의 모든 이점을 얻을 수 있으며, Navigation 컴포넌트가 FragmentTransition의 복잡함을 처리할 수 있다. 게다가, Navigation컴포넌트는 올바른 Up / Back 동작을 자동적으로 빌드되도록, Deep 링크를 지원하도록, NavigationDrawer / BottomNavigation 같은 UI위젯에서도 적절하게 네비게이셔닝이 되도록 도와준다. 안드로이드 스튜디오 3.2에서는 네비게이션 에디터를 추가하여 네비게이션 속성들을 보고 관리할 수 있도록 추가될 예정이다.


자세한 내용은 공식문서에서 확인할 수 있다.

Paging

앱에 표시되는 데이터는 양이 많고, 로드하는 동작은 비용이 크기 때문에, 다운로드나 생성, 한번에 너무 많이 보여주는 동작을 피해야한다. Paging 컴포넌트는 RecyclerView에서 많은 양의 데이터를 로드하고 나타내는 작업을 빠르게 수행할 수 있도록 도와준다. 페이징 된 데이터를 로컬 / 네트워크 / 둘다 에서 불러올 수 있으며, 어떻게 컨텐츠를 불러올지 정의할 수 있다. Room, LiveData, RxJava와 함께 사용될 수 있다.
자세한 내용은 공식문서에서 확인할 수 있다.

Slice

JetPack에 Slice 컴포넌트가 추가되었다. "Slice"는 특정 내용을 다른 앱에서 보여주는 개념이다. 다음의 예시는 구글 어시스턴트의 검색결과로 어시스턴트 UI안에 내용물을 렌더링한다.


자세한 내용은 공식문서에서 확인할 수 있다.

Android KTX

JetPack의 많은 목표중에 하나는 코틀린 언어의 이점을 이용해 앱 구현의 생산성을 높이는 것이다. Android KTX는 코틀린코드를 다음과 같이 변경할 수 있도록 도와준다.
view.viewTreeObserver.addOnPreDrawListener(
  object : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
      viewTreeObserver.removeOnPreDrawListener(this)
      actionToBeTriggered()
      return true
    }
});
더 간결하게 줄일 수 있다.
view.doOnPreDraw { actionToBeTriggered() }

이는 단지 JetPack을 이용하여 코틀린을 지원하는 시작일 뿐이다. 우리의 목표는 JetPack을 이용해 코틀린 개발자가 좋은 앱을 만드는 것이다.
자세한 내용은 공식문서에서 확인할 수 있다.

원문

https://android-developers.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html

2018년 4월 17일 화요일

[안드로이드] Architecture Component 3 - Paging Library 공식문서 번역

Paging Library

페이징 라이브러리는 앱이 데이터 소스로부터 필요한 정보를 점진적으로 읽어오는 작업을 쉽게 만들어주는 라이브러리로, 디바이스에 과부하가 걸리거나, 큰 데이터베이스로부터의 쿼리를 기다리지 않도록 만들어준다.

개요

대부분의 앱들은 많은 양의 데이터를 이용하여 동작하지만, 사실은 화면에 보여야 할 필요가 있는 일부분의 데이터만 불러와서 보여지면 된다. 예를들어, 어떤 앱에서 잠재적으로 보여줄 수 있는 데이터를 몇천개 가지고 있더라도, 한번에 몇십개씩만 엑세스하면 된다. 이부분을 앱이 신경쓰고 있지 않다면, 앱에서 불필요한 데이터까지 모두 요청하게 되고, 이는 불필요한 네트워크 트래픽의 발생과, 디바이스의 성능저하를 초래한다. 데이터가 원격저장소에 저장되거나 동기화되면, 이 또한 앱이 느려지는 원인이 되고, 사용자의 데이터요금을 낭비하게 된다.
기존의 Android API는 콘텐츠의 페이징을 허용했지만 중요한 제약 조건과 단점이 있었다.
  • CursorAdapter는 데이터베이스의 쿼리결과를 ListView의 항목들에 쉽게 매핑시켜주었지만, 데이터베이스 쿼리를 UI 스레드에서 하는것과 Cursor를 이용하기 때문에 데이터의 페이징을 비효율적으로 하는 단점이었다. 더 많은 정보를 확인하려면 Large Database Queries on Android 블로그 글을 확인한다.
  • AsyncListUtil은 RecyclerView에서 포지션 기반의 페이징을 허용하지만, 포지션 기반이 아닌 페이징은 허용하지 않는다. 또한, 셀수 있는 데이터셋에서 null을 플레이스 홀더로 강제로 사용한다.
새로운 페이징 라이브러리는 이러한 문제들을 해결한다. 이 라이브러리는 필요한 데이터를 요청하는 프로세스를 간소화하는 몇몇 클래스들을 포함한다. 이 클래스들은 Room과 같은 기존의 아키텍쳐 컴포넌트 라이브러리들과도 원활하게 잘 동작하도록 구성되어 있다.

기능

페이징 라이브러리의 클래스들을 사용하면, 이 섹션에서 소개되는 작업들을 수행하도록 도와준다.

데이터를 어떻게 가져오는지 정의하기

DataSource클래스를 이용하여 페이징된 데이터를 얻어올 데이터 소스를 정의한다. 데이터 접근 방법에 따라, 이 클래스를 상속받아 서브클래스로 구성할 수도 있다.
  • 로드할 데이터가 다음 / 이전 키를 포함하고 있다면, PageKeyedDataSource 클래스를 사용한다. 예를들어, 네트워크로부터 소셜미디어의 게시물을 가져올 경우, 연속적인 다음페이지를 로드하기 위해서, 다음페이지의 토큰을 전달해야 할 수도 있다.
  • 항목 N의 데이터를 이용해 N+1의 데이터를 가져와야 한다면, ItemKeyedDataSource클래스를 사용한다. 예를들어, 스레드된 코멘트들을 가져와야 한다면, 다음 코멘트를 가져오기 위해, 하나의 코멘트의 아이디를 전달해야 할 수도 있다.
  • 데이터 저장소에서 선택한 위치로부터 데이터의 페이지를 가져와야한다면, PositionalDataSource클래스를 사용한다. 이 클래스는 "1200번 위치로부터 20개의 데이터 반환"과 같이, 선택된 위치로부터 시작하는 데이터의 집합을 요청할 수 있다.
만약 Room 라이브러리를 사용하여 데이터를 관리하고 있다면, 이 Room 라이브러리는 자동적으로 PositionalDataSource인스턴스를 생성하기 위해 DataSource.Factory를 발생시킬 수 있다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);

메모리로부터 데이터 로드

PagedList클래스는 DataSource로부터 데이터를 불러옵니다. 개발자는 데이터가 한번에 얼마나 로드가 되어야 하고, 미리 패치되어야 하는지 설정하여, 사용자가 데이터로드가 완료될 때까지 기다려야 하는 시간을 최소화할 수 있다. 이 클래스는 RecyclerView.Adapter와 같은 다른 클래스에게 업데이트 신호를 제공하여, 페이지가 로드되었을 때 RecyclerView의 컨텐츠를 업데이트하도록 도와준다.
앱의 구조에 따라, PagedList 클래스 사용을 위한 몇몇 옵션들이 있다. 더 많은 내용을 보려면, 다음의 데이터 로딩 구조를 참고한다.

UI에 데이터 나타내기

PagedListAdapter 클래스는 PagedList로부터 데이터를 UI에 나타내기 위한 RecyclerView.Adapter의 구현체이다. 예를들어, 새로운 페이지가 로드되었을 때, PagedListAdapter는 데이터가 갱신되었음을 RecyclerView에게 알려준다. 이는 RecyclerView가 실제 항목으로 플레이스홀더를 대체하도록 하여 적절한 애니메이션을 수행할 수 있다.
또한 PagedListAdapter는 백그라운드 스레드를 사용하여 한 PagedList에서 다른 PagedList 로의 변경 사항을 계산하고 (예 : 데이터베이스 변경이 업데이트 된 데이터로 새 PagedList를 생성하는 경우) 필요에 따라 목록 내용을 업데이트하기 위해 notifyItem ... () 메소드를 호출한다. 그리고나서 RecyclerView는 필요한 변화를 수행한다. 예를들어, PagedList 버전사이에 항목들의 위치가 변경되면, RecyclerView는 해당 항목을 새로운 위치로 애니메이션으로 움직인다.

데이터 갱신 관찰하기

페이징 라이브러리는 실시간 업데이트가 가능한 PagedList 컨테이너를 생성하기 위한 클래스들을 제공하고 있다.
  • LivePageListBuilder : 이 클래스는 개발자가 제공한 DataSource.Factory를 통해 LiveData<PagedList>를 생성한다. 다음의 샘플코드에서 보는것처럼, 만약 데이터베이스를 관리하기 위해 Room 라이브러리를 사용한다면, DAO는 PositionalDataSource를 사용하여 DataSource.Factory를 생성할 수 있다.
LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();
  • RxpagedListBuilder : 이 클래스는 LivepagedListBuilder와 유사하게, RxJava2 기반의 기능을 제공한다. 이 클래스는 아키텍쳐 라이브러리의 RxJava2(android.arch.paging:rxjava2:1.0.0-alpha1)기반으로 제공된다. 다음의 코드에서 볼 수 있듯이, 이 클래스를 사용하여, PagedList를 구현할 때 Flowable과 Observable을 생성할 수 있다.
Flowable<PagedList<Item>> pagedItems =
        RxPagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchScheduler(myNetworkScheduler)
                .buildFlowable(BackpressureStrategy.LATEST);

데이터 흐름 생성하기

페이징 라이브러리의 컴포넌트는 백그라운드 스레드의 생산자로부터 UI스레드의 화면 출력까지의 데이터 흐름을 구성한다. 예를들어, 새로운 항목이 데이터베이스에 추가되면, DataSource는 갱신될 것이고, LiveData<PagedList> / Flowable<PagedList> 는 백그라운드 스레드에서 새로운 PagedList를 생산한다.
paging-threading.gif
Figure1 : 페이징 라이브러 컴포넌트들은 대부분의 작업을 백그라운드 스레드에서 수행한다. 그래서 UI스레드에 큰 부담이 되지 않는다.
새로 생성된 PagedList는 UI스레드에서 PagedListAdapter로 보내진다. PagedListAdapter는 백그라운드 스레드에서 DiffUtil을 이용하여, 기존의 리스트와 새로운 리스트 사이의 차이를 계산한다. 비교가 끝났으면, PagedListAdapter는 리스트의 차이점을 이용하여 RecyclerView.Adapter.notifyItemInserted() 를 호출하여 UI에 항목들을 삽입한다.
UI스레드의 RecyclerView는 삽입된 아이템을 확인하여 적절하게 애니메이셔닝 한다.

데이터베이스 예제

다음의 코드조각들은 모든 라이브러리들이 함께 동작하도록 하는 몇몇 방법을 보여준다.

LiveData를 이용하여 페이징된 데이터 관찰하기

다음의 코드는 모든 컴포넌트들이 함께 동작하는 것을 보여준다. 사용자가 데이터베이스에 추가, 삭제, 변경할 때, RecyclerView의 내용은 자동적으로, 효율적으로 갱신된다.
@Dao
interface UserDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract DataSource.Factory<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = new LivePagedListBuilder<>(
                userDao.usersByLastName(), /* page size */ 20).build();
    }
}

class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList ->
                mAdapter.submitList(pagedList));
        recyclerView.setAdapter(mAdapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}

RxJava2를 이용하여 페이징된 데이터 관찰하기

LiveData대신 RxJava2를 사용하려고 한다면, Observable이나 Flowable 객체를 생성한다.
class MyViewModel extends ViewModel {

    public final Flowable<PagedList<User>> usersList;

    public MyViewModel(UserDao userDao) {
        usersList = new RxPagedListBuilder<>(userDao.usersByLastName(),
                /* page size */ 50).buildFlowable(BackpressureStrategy.LATEST);
    }
}
그리고, 다음의 코드와 같이 해당 데이터의 관찰을 시작 / 중지할 수 있다.
class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;
    private final CompositeDisposable mDisposable = new CompositeDisposable();

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        myDisposable.add(mViewModel.usersList.subscribe(flowableList ->
                mAdapter.submitList(flowableList)));
    }

    @Override
    protected void onStop() {
        super.onStop();
        mDisposable.clear();
    }
}
UserDaoUserAdapter 클래스의 코드는 LiveData의 예제와 같으므로 생략한다.

데이터 로딩 구조 선택하기

페이징 라이브러리를 이용해 데이터를 페이징하는 두가지의 기본적인 방법이 있다.

네트워크 or 데이터베이스

첫번째로, 하나의 소스(로컬저장소이든 네트워크이든)로부터 페이징을 할 수 있다. 이 경우 다음 샘플과 같이 LiveData를 이용하여 로드된 데이터를 UI에 전달한다.
데이터의 소스를 지정하려면 LivePagedListBuilder에 DataSource.Factory를 전달한다.
paging-network-or-database.png
Figure2 : 하나의 데이터소스는 DataSource.Factory를 제공하여 컨텐츠를 로드한다.
데이터베이스를 관찰할 때, 컨텐츠의 변화가 발생하면, 데이터베이스는 새로운 PagedList에 푸쉬한다. 네트워크 페이징인 경우에(백엔드가 업데이트를 보내지 않을 때), 스와이프 리프레시 같은 시그널은 현재 데이터소스를 무효화함으로써 새로운 PagedList를 받아올 수 있다. 이는 모든 데이터를 비동기적으로 새로고침한다.
PagingWithNetworkSample의 메모리 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크 DataSource.Factory를 구현했는지를 보여준다.

네트워크 and 데이터베이스

두번째로, 로컬저장소로부터 데이터를 페이지할 수 있고, 로컬저장소는 네트워크로부터 추가 데이터를 페이징한다. 이는 종종 네트워크 연결을 최소화하고, 백엔드의 캐시로 데이터베이스를 사용하여 더 나은 연결지양 경험을 제공한다.
이 경우, LiveData를 이용하여 데이터베이스로부터 컨텐츠를 페이징하고, 최신 데이터의 시그널을 관찰하기 위해, LivePagedListBuilder에 BoundaryCallback을 전달한다.
paging-network-plus-database.png
Figure3 : 데이터베이스는 네트워크 데이터의 캐시로 이용된다. UI는 데이터베이스로부터 데이터를 로드하고, 데이터가 없을 때, 네트워크로 신호를 보내서 네트워크로부터 데이터베이스로 데이터를 로드한다.
이후, 이 콜백을 네트워크 요청에 연결하면 데이터가 데이터베이스에 바로 저장된다. UI는 데이터베이스의 갱신을 구독하고 있으므로, 새로운 데이터의 변경은 자동적으로 UI로 전달된다.
PagingWithNetworkSample의 데이터베이스 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크의 BoundaryCallback을 구현하는지 보여준다.

원문 : https://developer.android.com/topic/libraries/architecture/paging.html