天天看點

Jetpack 架構元件 Paging 分頁加載 MD

Markdown版本筆記 我的GitHub首頁 我的部落格 我的微信 我的郵箱
MyAndroidBlogs baiqiantao bqt20094 [email protected]

目錄

Paging

簡介

使用步驟

PageKeyedDataSource

Java版案例

PagingActivity

PagingAdapter

PagingViewModel

User

UserDao

UserDb

官方文檔

官方案例

Paging 是什麼?

Paging 可以使開發者更輕松在 RecyclerView 中 分頁加載資料。

implementation "android.arch.paging:runtime:1.0.1" //Paging
implementation "android.arch.paging:rxjava2:1.0.1" //Paging對RxJava2的支援           

原理示意圖:https://upload-images.jianshu.io/upload_images/7293029-27facf0a399c66b8.gif?imageMogr2/auto-orient/

組成部分:

  • DataSource:資料源,

    資料的改變會驅動清單的更新

    ,是以,資料源是很重要的
  • PageList:核心類,它從資料源取出資料,同時,它負責控制

    第一次預設加載多少資料

    ,之後

    每一次加載多少資料

    ,如何加載等等,并将資料的變更反映到UI上。
  • PagedListAdapter:擴充卡,RecyclerView的擴充卡,通過分析資料是否發生了改變,負責處理UI展示的邏輯(增加/删除/替換等)。

建立資料源

在Paging中,資料源被抽象為 DataSource , 其擷取需要依靠 DataSource 的内部工廠類

DataSource.Factory

,通過create()方法就可以獲得DataSource 的執行個體:

public abstract static class Factory<Key, Value> {
     public abstract DataSource<Key, Value> create();
}           

資料源一般有兩種選擇,遠端伺服器請求或者讀取本地持久化資料,這些并不重要,本文我們以Room資料庫為例:

@Query("SELECT * FROM table_user")
DataSource.Factory<Integer, User> getAllUserDataSource();           
DataSource.Factory<Integer, User> factory = UserDb.get(getApplication()).userDao().getAllUserDataSource();           

Paging可以獲得Room的原生支援,是以作為示例非常合适,當然我們更多擷取資料源是通過API網絡請求,其實作方式可以參考 官方Sample。

PS:如果通過API網絡請求擷取DataSource,相比使用Room來說要麻煩很多

配置PageList

PageList的作用:

  • 從資料源取出資料
  • 負責控制第一次預設加載多少資料,之後每一次加載多少資料,如何加載等等
  • 将資料的變更反映到UI上

PageList提供了 PagedList.Config 類供我們進行執行個體化配置,其提供了5個可選配置:

public static final class Builder {
    //  省略Builder其他内部方法 
    private int mPageSize = -1;    //每次加載多少資料
    private int mPrefetchDistance = -1;   //距底部還有幾條資料時,加載下一頁資料
    private int mInitialLoadSizeHint = -1; //第一次加載多少資料,必須是分頁加載數量的倍數
    private boolean mEnablePlaceholders = true; //是否啟用占位符,若為true,則視為固定數量的item
    private int mMaxSize = MAX_SIZE_UNBOUNDED; //預設Integer.MAX_VALUE,Defines how many items to keep loaded at once.
}           

配置Adapter

就像我們平時配置 RecyclerView 差不多,我們配置了 ViewHolder 和 RecyclerView.Adapter,略微不同的是,我們需要繼承

PagedListAdapter

,并且我們需要傳一個 DifffUtil.ItemCallback 的執行個體。

DifffUtil.ItemCallback的意義是,我需要知道怎麼樣的比較,才意味着資料源的變化,并根據變化再進行的UI重新整理操作。

監聽資料源的變更,并響應在UI上

這個就很簡單了

//每當觀察到資料源中資料的變化,我們就把最新的資料交給Adapter去展示
viewModel.getRefreshLiveData().observe(this, pagedList -> {
    Log.i("bqt", "【資料發生改變】" + pagedList.size() + " "
        + pagedList.getPositionOffset() + " " + pagedList.getLoadedCount() + " " + pagedList.getLastKey() + " "
        + pagedList.isImmutable() + " " + pagedList.isDetached());
    adapter.submitList(pagedList); //将資料的變化反映到UI上 Set the new list to be displayed
});           

參考

基本結構:

//這個資料源主要需要傳遞Int型的PageNum作為參數實作每一頁資料的請求
public class PagingDataSource extends PageKeyedDataSource<Integer, User> {

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, User> callback) {
        //requestedLoadSize為加載的資料量,placeholdersEnabled是是否顯示占位;callback為資料加載完成的回調
        //LoadInitialCallback的onResult方法有三個參數,第一個為資料,後面兩個即為上一頁和下一頁
        Log.i("bqt", "【loadInitial】" + params.requestedLoadSize + " " + params.placeholdersEnabled);//初始加載資料

        //if(滿足條件) 請求一批資料,資料處理後通過callback傳回
        //callback.onResult(List<Value> data, Key previousPageKey, Key nextPageKey);
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, User> callback) {
        Log.i("bqt", "【loadBefore】" + params.key + " " + params.requestedLoadSize);//向前分頁加載資料
        //key即為DataSource<Key, Value>中的key,在這裡即為頁數;同樣,callback為資料加載完成的回調
        //LoadParams中的key即為我們要加載頁的資料,加載完後回調中告知下一次加載資料頁數+1或者-1
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, User> callback) {
        Log.i("bqt", "【loadAfter】" + params.key + " " + params.requestedLoadSize);//向後分頁加載資料
        //if(滿足條件) 再請求一批資料,資料處理後通過callback傳回
        //callback.onResult(List<Value> data, Key adjacentPageKey);
    }
}           

繼承自

PageKeyedDataSource

後需要實作以下三個方法:

  • loadInitial

    初始加載資料
  • loadAfter

    向後分頁加載資料
  • loadBefore

    向前分頁加載資料

這三個方法都有兩個參數,一個

params

和一個

callback

  • params包裝了分頁加載的參數:
    • loadInitial中的params為

      LoadInitialParams

      包含了requestedLoadSize和placeholdersEnabled兩個屬性,requestedLoadSize為加載的資料量,placeholdersEnabled是是否顯示占位及當資料為null時顯示占位的view
    • loadBefore和loadAfter中的params為

      LoadParams

      包含了key和requestedLoadSize,key即為

      DataSource<Key, Value>

      中的key,在這裡即為頁數
  • callback為資料加載完成的回調,loadInitial中調用調用IPVTApiPresenter加載資料,然後調用

    callback.onResult

    告訴調用者資料加載完成。

onResult有三個參數,第一個為資料,後面兩個即為上一頁和下一頁。

如果我們目前頁為第一頁即沒有上一頁,則上一頁為null,下一頁為2,此時加載的時候會加載目前頁和調用loadAfter加載第二頁,但不會調用loadBefore,因為沒有上一頁,即previousPageKey為null不會加載上一頁

如果我們初始加載的是第三頁,則上一頁是2,下一頁是4,此時加載的時候會加載目前頁和調用loadAfter加載第4頁,調用loadBefore加載第二頁

分頁加載的時候會将previousPageKey或nextPageKey傳遞到loadAfter或loadBefore中的

params.key

loadAfter 、loadBefore中的params中的key即為我們要加載頁的資料,加載完後回調中告知下一次加載資料頁數+1或者-1

參考 此部落格,原文為Kotlin版案例,我将其轉為Java版實作,并在其基礎上添加了一些邏輯。

public class PagingActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_paging);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);

        DiffUtil.ItemCallback<User> itemCallback = new DiffUtil.ItemCallback<User>() {
            @Override
            public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) {
                return oldItem.uid == newItem.uid;
            }

            @Override
            public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
                return oldItem == newItem;
            }
        };
        PagingAdapter adapter = new PagingAdapter(itemCallback);

        UserDao dao = UserDb.get(this).userDao();
        adapter.setOnClick((user, position) -> {
            Log.i("bqt", "【position】" + position);
            new Thread(() -> {
                if (position % 2 == 0) dao.deleteUser(user);
                else dao.insertUser(new User("insert"));
            }).start();
        });

        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        dao.getAllUser().observe(this, users -> Log.i("bqt", "【資料發生改變】" + users.size()));
        PagingViewModel viewModel = ViewModelProviders.of(this).get(PagingViewModel.class);

        //每當觀察到資料源中資料的變化,我們就把最新的資料交給Adapter去展示
        viewModel.getRefreshLiveData().observe(this, pagedList -> {
            Log.i("bqt", "【資料發生改變】" + pagedList.size() + " "
                + pagedList.getPositionOffset() + " " + pagedList.getLoadedCount() + " " + pagedList.getLastKey() + " "
                + pagedList.isImmutable() + " " + pagedList.isDetached());
            adapter.submitList(pagedList); //将資料的變化反映到UI上 Set the new list to be displayed
        });
    }
}           

public class PagingAdapter extends PagedListAdapter<User, PagingAdapter.MyViewHolder> {

    PagingAdapter(DiffUtil.ItemCallback<User> itemCallback) {
        super(itemCallback);
    }

    @Override
    public void onCurrentListChanged(@Nullable PagedList<User> previousList, @Nullable PagedList<User> currentList) {
        super.onCurrentListChanged(previousList, currentList);
        Log.i("bqt", "【onCurrentListChanged】");
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_student, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        Log.i("bqt", "【onBindViewHolder】" + position);
        User user = getItem(position);
        //items might be null if they are not paged in yet. PagedListAdapter will re-bind the ViewHolder when Item is loaded.
        if (user != null) {
            holder.nameView.setText(user.name);
            holder.nameView.setOnClickListener(v -> {
                if (onClick != null) {
                    onClick.onClick(user, position);
                }
            });
        }
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        TextView nameView;

        MyViewHolder(View view) {
            super(view);
            nameView = view.findViewById(R.id.name);
        }
    }

    private OnClick onClick;

    void setOnClick(OnClick onClick) {
        this.onClick = onClick;
    }

    interface OnClick {
        void onClick(User user, int position);
    }
}           

public class PagingViewModel extends AndroidViewModel {

    public PagingViewModel(@NonNull Application application) {
        super(application);
    }

    public LiveData<PagedList<User>> getRefreshLiveData() {
        DataSource.Factory<Integer, User> dataSourceFactory = UserDb.get(getApplication()).userDao().getAllUserDataSource();

        PagedList.Config config = new PagedList.Config.Builder()
            .setInitialLoadSizeHint(10)  //第一次加載多少資料,必須是分頁加載數量的倍數
            .setPageSize(5)  //每次加載多少資料
            .setMaxSize(Integer.MAX_VALUE) //Defines how many items to keep loaded at once.
            .setPrefetchDistance(5) //距底部還有幾條資料時,加載下一頁資料
            .setEnablePlaceholders(true) //是否啟用占位符,若為true,則視為固定數量的item
            .build();

        LivePagedListBuilder<Integer, User> livePagedListBuilder = new LivePagedListBuilder<>(dataSourceFactory, config)
            .setFetchExecutor(Executors.newSingleThreadExecutor()) //設定擷取資料源的線程
            .setInitialLoadKey(0) //可通過 pagedList.getLastKey() 擷取此值,預設值當然為 Key(這裡為Integer)類型的初始化值()這裡為0
            .setBoundaryCallback(new PagedList.BoundaryCallback<User>() {
                @Override
                public void onZeroItemsLoaded() { //沒有資料被加載
                    super.onZeroItemsLoaded();
                    Log.i("bqt", "【onZeroItemsLoaded】");
                }

                @Override
                public void onItemAtFrontLoaded(@NonNull User itemAtFront) { //加載第一個
                    super.onItemAtFrontLoaded(itemAtFront);
                    Log.i("bqt", "【onItemAtFrontLoaded】" + itemAtFront.name);
                }

                @Override
                public void onItemAtEndLoaded(@NonNull User itemAtEnd) { //加載最後一個
                    super.onItemAtEndLoaded(itemAtEnd);
                    Log.i("bqt", "【onItemAtEndLoaded】" + itemAtEnd.name);
                }
            });
        return livePagedListBuilder.build();
    }
}           

@Entity(tableName = "table_user")
public class User {
    @PrimaryKey(autoGenerate = true) public int uid;
    @ColumnInfo(name = "user_name") public String name = "包青天";

    public User(String name) {
        this.name = name;
    }
}           

@Dao
public interface UserDao {

    @Insert
    List<Long> insertUser(User... users);

    @Insert
    List<Long> insertUser(List<User> users);

    @Delete
    int deleteUser(User user);

    @Query("SELECT * FROM table_user")
    LiveData<List<User>> getAllUser();

    @Query("SELECT * FROM table_user")
    DataSource.Factory<Integer, User> getAllUserDataSource();
}           

@Database(entities = {User.class}, version = 1)
public abstract class UserDb extends RoomDatabase {
    public abstract UserDao userDao(); //沒有參數的抽象方法,傳回值所代表的類必須用@Dao注解

    private static UserDb db;

    public static UserDb get(Context context) {
        if (db == null) {
            db = Room.databaseBuilder(context.getApplicationContext(), UserDb.class, "dbname")
                .addCallback(new RoomDatabase.Callback() {
                    @Override
                    public void onCreate(@NonNull SupportSQLiteDatabase database) {
                        super.onCreate(database);
                        Log.i("bqt", "【onCreate】");
                        new Thread(() -> {
                            List<User> users = new ArrayList<>();
                            for (int i = 0; i < 50; i++) {
                                users.add(new User("bqt" + i));
                            }
                            get(context).userDao().insertUser(users);
                        }).start();
                    }
                })
                .build();
        }
        return db;
    }
}           

2019-4-7

附件清單

本文來自部落格園,作者:白乾濤,轉載請注明原文連結:https://www.cnblogs.com/baiqiantao/p/10668184.html

繼續閱讀