iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 23
0
Software Development

Android Architecture系列 第 23

Architecture Components - Paging

Architecture Components中還有一個比較晚登場的成員Paging,目前還在alpha階段所以沒有規劃進專案中,於今天單獨介紹它。

Why Paging?

考量Android裝置的螢幕通常不會太大,App即便有上千筆資料其實同時只會顯示十幾筆,在撈資料時如果處理不好就會造成效能的浪費。

Paging讓我們可以從大量資料先撈出一小部分顯示,並依照滑動列表等情況自動在background thread撈出新的資料更新UI,降低效能和記憶體的消耗。同時Paging也和其他Architecture Components成員整合,一起使用時只要改很少的程式就能套用。

Paging

Paging由幾個class組成,簡略說明如下:

  1. DataSource:資料來源。
  2. PagedList:自DataSource取得資料,可以設定單次取得和預先取得(prefetch)的資料量。
  3. PagedListAdapter:顯示PagedList的資料,資料異動時會在background thread計算資料差異以更新UI。
  4. LivePagedListBuilder:將PagedList和LiveData整合變成LiveData<PagedList>

運作流程:

              Paging Library components

我說明能力不好所以上面都看不懂也沒關係,我們直接看程式,先加入dependencies:

implementation "android.arch.paging:runtime:1.0.0-alpha4-1"

修改DAO:

@Query("SELECT * FROM Repo WHERE id in (:repoIds)")
public abstract DataSource.Factory<Integer, Repo> loadById(List<Integer> repoIds);

Room會自動透過DataSource.Factory產生PositionalDataSource,這種DataSource讓我們從任意位置開始撈取資料,例如撈出第1000筆之後的50筆。

修改Repository:

public class RepoRepository {

    ...

    public LiveData<Resource<PagedList<Repo>>> search(final String query) {
        return new NetworkBoundResource<PagedList<Repo>, RepoSearchResponse>() {
            @NonNull
            @Override
            protected LiveData<PagedList<Repo>> loadFromDb() {
                return Transformations.switchMap(repoDao.search(query), new Function<RepoSearchResult, LiveData<PagedList<Repo>>>() {
                    @Override
                    public LiveData<PagedList<Repo>> apply(RepoSearchResult searchData) {
                        return new LivePagedListBuilder<>(repoDao.loadById(searchData.repoIds), 30).build();
                    }
                });
            }

            @Override
            protected boolean shouldFetch(@Nullable PagedList<Repo> data) {
                return data == null;
            }

            ...
        }.asLiveData();
    }
}

原本回傳型態由List<Repo>改成PagedList<Repo>,loadFromDb中使用LivePagedListBuilder將撈出來的PagedList用LiveData包裝變成LiveData<PagedList<Repo>>,30是要撈的資料數量(PageSize)。

LivePagedListBuilder可以用config做更多設定,例如:

PagedList.Config config = new PagedList.Config.Builder()
                                .setPageSize(30)
                                .setPrefetchDistance(20)
                                .setEnablePlaceholders(true)
                                .build();
LivePagedListBuilder<>(repoDao.loadById(searchData.repoIds), config).build();

若沒有用config設定而是像Repository中直接寫PageSize數字的話,PrefetchDistance默認等於PageSize。

ViewModel只要改LiveData的資料型態就好囉,將原本的List<Repo>改成PagedList<Repo>

public class RepoViewModel extends ViewModel {

    private final LiveData<Resource<PagedList<Repo>>> repos;

    ...

    @Inject
    public RepoViewModel(RepoRepository repoRepository) {
        ...
        repos = Transformations.switchMap(query, new Function<String, LiveData<Resource<PagedList<Repo>>>>() {
            @Override
            public LiveData<Resource<PagedList<Repo>>> apply(String userInput) {
                ...
            }
        });
    }

    LiveData<Resource<PagedList<Repo>>> getRepos() {
        return repos;
    }
}

建立PagedListAdapter:

public class RepoAdapter extends PagedListAdapter<Repo, RepoAdapter.RepoViewHolder> {

    RepoAdapter() {
        super(DIFF_CALLBACK);
    }

    class RepoViewHolder extends RecyclerView.ViewHolder{

        private final RepoItemBinding binding;

        RepoViewHolder(RepoItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        void bind(Repo repo) {
            binding.setRepo(repo);
            binding.executePendingBindings();
        }
    }

    @Override
    public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
        RepoItemBinding binding = RepoItemBinding.inflate(layoutInflater, parent, false);
        return new RepoViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(RepoViewHolder holder, int position) {
        Repo repo = getItem(position);
        holder.bind(repo);
    }

    private static final DiffCallback<Repo> DIFF_CALLBACK = new DiffCallback<Repo>() {
        @Override
        public boolean areItemsTheSame(@NonNull Repo oldRepo, @NonNull Repo newRepo) {
            return Objects.equals(oldRepo.id, newRepo.id);
        }
        @Override
        public boolean areContentsTheSame(@NonNull Repo oldRepo, @NonNull Repo newRepo) {
            return Objects.equals(oldRepo.name, newRepo.name) &&
                    Objects.equals(oldRepo.description, newRepo.description);
        }
    };
}

需建立DIFF_CALLBACK,這像是精簡版的DiffUtil,比較好寫之外還會自動在background thread計算資料差異。原本的getItemCount()那些不用寫了,PagedListAdapter會幫我們處理,並提供內建的method如getItem(position)讓我們取得Repo,整體比RecyclerView Adapter還簡潔。

最後於View中接收資料:

viewModel.getRepos().observe(this, new Observer<Resource<PagedList<Repo>>>() {
    @Override
    public void onChanged(@Nullable Resource<PagedList<Repo>> resource) {
        ...
        repoAdapter.setList(resource.data);
    }
});

使用PagedListAdapter提供的setList(PagedList)就會觸發Adapter計算資料差異並更新UI。


Paging目前還在alpha-4階段,之後除了資料庫分頁之外也會支援網路連線的分頁,有相關需求的話可再持續追蹤。現在就導入專案的話要稍留意後續的API異動,更新時有可能會有deprecated的內容。

GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day23-paging


上一篇
小結
下一篇
Data Binding Compiler V2
系列文
Android Architecture30

尚未有邦友留言

立即登入留言