Android 基於 MVP 框架的下拉重新整理、上拉載入頁面,View和Presenter層基類封裝
前言
Android 專案開發中經常遇到列表式頁面,並且需要實現下拉重新整理,上拉到底後加載下一頁的功能,這裡結合我們專案正在使用的 MVP 框架,介紹一種基類封裝方案,實現 View、Adapter、資料處理Presenter層的基類封裝,後續繼承這幾個類,簡單地重寫下 UI 佈局,網路請求即可實現下拉重新整理,上拉載入功能。
View 層封裝
View 層我們封裝了 BaseScrollActivity 和 BaseScrollFragment 兩個基類,分別用在需要使用 Activity 和 Fragment 的地方,這裡先介紹下 BaseScrollActivity 。
UI 佈局
要求所有繼承的子類 Activity 必須包含一個 SwipeRefreshLayout ,再在其內部包含一個 RecyclerView。SwipeRefreshLayout 用於實現下拉重新整理,而上拉載入需要通過 RecyclerView 的 OnScrollListener 實現。
<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refreshLayout"
android:layout_width="match_parent" android:layout_height="match_parent" >
<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
BaseScrollActivity 封裝
再看一下 BaseScrollActivity 的程式碼:
package chenyu.jokes.base;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import chenyu.jokes.R;
import java.util.ArrayList;
import nucleus.view.NucleusAppCompatActivity;
/**
* Created by chenyu on 2017/5/15.
*/
public abstract class BaseScrollActivity<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
extends NucleusAppCompatActivity<P> implements BaseRxView<M> {
@BindView(R.id.recyclerView) public RecyclerView recyclerView;
@BindView(R.id.refreshLayout) public SwipeRefreshLayout refreshLayout;
private int currentPage = 1;
private int previousTotal = 0;
private boolean loading = true;
private boolean noMoreData = false;
protected Adapter mAdapter;
protected boolean needLoadMore = true;
public abstract int getLayout();
public abstract Adapter getAdapter();
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayout());
ButterKnife.bind(this);
mAdapter = getAdapter();
recyclerView.setAdapter(mAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
}
@Override protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
initListener();
getPresenter().loadPage(1);
}
private void initListener() {
refreshLayout.setColorSchemeResources(R.color.colorPrimary);
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override public void onRefresh() {
mAdapter.clear();
getPresenter().loadPage(1);
currentPage = 1;
previousTotal = 0;
mAdapter.notifyDataSetChanged();
refreshLayout.setRefreshing(false);
}
});
if (needLoadMore) {
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (noMoreData) {
return;
}
int totalItemCount = recyclerView.getAdapter().getItemCount();
int lastVisibleItem =
((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && lastVisibleItem >= totalItemCount - 1) {//(totalItemCount - visibleItemCount) <= firstVisibleItem
loading = true;
currentPage++;
onLoadMore();
previousTotal = totalItemCount;
}
}
});
}
}
@Override public void onItemsNext(ArrayList<M> items) {
if (items.isEmpty()) {
noMoreData = true;
loading = false;
return;
}
mAdapter.addAll(items);
mAdapter.notifyDataSetChanged();
loading = false;
}
@Override public void onItemsError(Throwable throwable) {
Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
public void onLoadMore() {
getPresenter().loadPage(currentPage);
}
@Override protected void onDestroy() {
super.onDestroy();
mAdapter.clear();
}
}
類定義
首先看下類的定義:
public abstract class BaseScrollActivity<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
extends NucleusAppCompatActivity<P> implements BaseRxView<M>
我們定義的一個抽象類,因為有兩個抽象函式需要子類去實現,分別是:
public abstract int getLayout();
public abstract Adapter getAdapter();
getLayout()
用於指定 layout 資源,getAdapter()
用於指定 RecyclerView 的 Adapter,子類裡直接 return 需要的值就行。
父類 nucleus.view.NucleusAppCompatActivity 來自 nucleus。Nucleus 是一個 Android MVP 框架,具體用法可以參考我之前的博文:使用MVP+Retrofit+RxJava實現的的Android Demo (上)使用Nuclues庫實現MVP
用到了3個泛型:<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
分別是 RecyclerView 需要用到的 Adapter ,Presenter, 資料模型 M,除了M,都是繼承自我們自己封裝的基類。
還有一個介面 implements BaseRxView<M>
,程式碼如下:
package chenyu.jokes.base;
import java.util.ArrayList;
/**
* Created by chenyu on 2017/5/20.
*/
public interface BaseRxView<Model> {
void onItemsNext(ArrayList<Model> model);
void onItemsError(Throwable throwable);
}
兩個函式,分別在資料請求成功和失敗時呼叫,單獨把這兩個提取到一個接口裡,主要是為了使 BaseScrollActivity 和 BaseScrollFragment 能實現同一個介面,後面可以只封裝一個 Presenter 類。
初始化
接下來變數宣告,在 onCreate() 函式裡進行 RecyclerView 的初始化,包括給 mAdapter 賦值並設定給 RecyclerView,LayouManager的設定。
載入首頁資料,新增監聽器
然後在 onPostCreate() 裡初始化下拉和上拉的 Listener,並通過getPresenter().loadPage(1);
語句,呼叫 Presenter 的方法來載入第一頁的資料。
為什麼不放在 onCreate() 裡呢?這是考慮到子類的 onCreate() 裡可能還會有其他的初始化操作,比如基類變數protected boolean needLoadMore = true;
這個是用來控制是否新增上拉載入監聽器的,預設為 true,考慮到有些時候可能只要下拉重新整理,但資料的獲取沒有分頁,不需要上拉載入更多,那麼子類可以在 onCreate() 裡把 needLoadMore 設定成false。這個需要在 initListener() 之前執行,如果基類中把 initListener() 放在onCreate() 裡,那子類只能在呼叫 super.onCreate() 之前對 needLoadMore 進行賦值了,雖然也能實現效果,但是不優雅。
另外子類也可能需要對 Presenter 進行一些初始化,需要在載入第一頁的資料之前執行,因此getPresenter().loadPage(1);
也要放在 onPostCreate() 裡。
放到onStart()、onResume() 也是不合適的,因為這兩個回撥可能在 Activity 生命週期裡可能被回撥多次,但是新增 Listener 和載入首頁資料,只需要執行一次,onPostCreate() 是最佳選擇。
再看一下下拉重新整理監聽器的程式碼:
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override public void onRefresh() {
mAdapter.clear();
getPresenter().loadPage(1);
currentPage = 1;
previousTotal = 0;
mAdapter.notifyDataSetChanged();
refreshLayout.setRefreshing(false);
}
});
這個實現一下 SwipeRefreshLayout 自帶的監聽介面就可以,注意要先將Adapter的資料情況,再重新去載入第一頁資料,否則老的資料並沒有被重新整理,只是把新資料加到了最後面。同時要將各種翻頁要用到的變數復位到初始值。
再看下上拉載入下一頁的 Listener 程式碼:
if (needLoadMore) {
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (noMoreData) {
return;
}
int totalItemCount = recyclerView.getAdapter().getItemCount();
int lastVisibleItem =
((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && lastVisibleItem >= totalItemCount - 1) {
loading = true;
currentPage++;
onLoadMore();
previousTotal = totalItemCount;
}
}
});
}
這裡主要是用了RecyclerView 的 OnScrollListener,在滑動 RecyclerView 列表時進行檢測,如果列表中最後一個可見元素的 ID 是 總元素個數減一,則認為列表已經被拉到最低端,這是將 currentPage自加一,並呼叫 onLoadMore() 函式來載入下一頁資料。
而LoadMore() 也是呼叫 Presenter 的函式:
public void onLoadMore() {
getPresenter().loadPage(currentPage);
}
另外還有幾個 Boolean 變數來進行控制載入流程:
if (needLoadMore) { ... }
needLoadMore,用於控制是否新增上拉載入 Listener,預設為 true,如果子類中設定為 false,則不新增Listener,用於資料一次性載入完成,不需要分頁載入的場景。
noMoreData,沒有下一頁資料,初始化為false,如果載入下一頁時獲得的是空資料,說明已經載入完全部資料,沒有下一頁了,則置為true,為true時,Listener直接返回,不執行任何動作。
if (noMoreData) {
return;
}
loading ,表示是否正在請求資料,啟動載入下一頁前置為 true,載入完成後置為false,如果loading 為 true,觸發監聽器時,不會執行載入動作,主要為了防止網路不好,載入緩慢時,上拉到底會多次觸發載入同一頁的問題。
資料請求結束後的操作:
@Override public void onItemsNext(ArrayList<M> items) {
if (items.isEmpty()) {
noMoreData = true;
loading = false;
return;
}
mAdapter.addAll(items);
mAdapter.notifyDataSetChanged();
loading = false;
}
@Override public void onItemsError(Throwable throwable) {
Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
onItemsNext,onItemsError 這個兩個函式由Presenter在完成請求後選擇呼叫哪個,如果請求成功,則呼叫 onItemsNext,首先會判斷下資料是否為空,如果為空,則將noMoreData 置為 true,如果不為空,則將資料新增到Adapter中,更新 UI,將loading 置為 false。
BaseScrollFragment 封裝
BaseScrollActivity 基本就封裝這些,BaseScrollFragment 基本是一樣的,主要是Fragment和Activity生命週期不同,對應程式碼的執行位置也不同,這裡只貼一下程式碼:
package chenyu.jokes.base;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import chenyu.jokes.R;
import java.util.ArrayList;
import nucleus.view.NucleusSupportFragment;
/**
* Created by chenyu on 2017/3/6.
*/
public abstract class BaseScrollFragment<Adapter extends BaseScrollAdapter, P extends BaseScrollPresenter, M>
extends NucleusSupportFragment<P> implements BaseRxView<M> {
@BindView(R.id.recyclerView) public RecyclerView recyclerView;
@BindView(R.id.refreshLayout) public SwipeRefreshLayout refreshLayout;
private int currentPage = 1;
private int previousTotal = 0;
private boolean loading = true;
private boolean noMoreData = false;
protected Adapter mAdapter;
protected SwipeRefreshLayout.OnRefreshListener listener;
public abstract int getLayout();
public abstract Adapter getAdapter();
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(getLayout(), container, false);
return view;
}
@Override public void onViewCreated(View view, Bundle state) {
super.onViewCreated(view, state);
ButterKnife.bind(this, view);
mAdapter = getAdapter();
recyclerView.setAdapter(mAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
initListener();
getPresenter().loadPage(1);
}
private void initListener() {
refreshLayout.setColorSchemeResources(R.color.colorPrimary);
listener = new SwipeRefreshLayout.OnRefreshListener() {
@Override public void onRefresh() {
mAdapter.clear();
getPresenter().loadPage(1);
currentPage = 1;
previousTotal = 0;
mAdapter.notifyDataSetChanged();
refreshLayout.setRefreshing(false);
}
};
refreshLayout.setOnRefreshListener(listener);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (noMoreData) {
return;
}
int totalItemCount = recyclerView.getAdapter().getItemCount();
int lastVisibleItem =
((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && lastVisibleItem >= totalItemCount - 1) {
loading = true;
currentPage++;
onLoadMore();
previousTotal = totalItemCount;
}
}
});
}
@Override public void onItemsNext(ArrayList<M> items) {
if (items.isEmpty()) {
noMoreData = true;
loading = false;
return;
}
mAdapter.addAll(items);
mAdapter.notifyDataSetChanged();
loading = false;
}
@Override public void onItemsError(Throwable throwable) {
Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
public void onLoadMore() {
getPresenter().loadPage(currentPage);
}
@Override public void onDestroyView() {
super.onDestroyView();
mAdapter.clear();
}
}
Adapter 封裝
RecyclerView的Adapter,為了減少重複程式碼,我們也提取一些公共操作進行封裝,先上程式碼:
package chenyu.jokes.base;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import butterknife.ButterKnife;
import java.util.ArrayList;
/**
* Created by chenyu on 2017/3/3.
*/
public abstract class BaseScrollAdapter<Model, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
protected ArrayList<Model> mItems = new ArrayList<>();
protected ViewGroup parent;
public abstract int getLayout();
@Override public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(
getLayout(),parent,false);
this.parent = parent;
return getViewHolder(view);
}
protected abstract VH getViewHolder(View view) ;
@Override public void onBindViewHolder(VH holder, int position){
ButterKnife.bind(this,holder.itemView);
}
@Override public int getItemCount() {
return mItems.size();
}
public void addAll(ArrayList<Model> items) {
mItems.addAll(items);
}
public void add(Model item) {
mItems.add(item);
}
public void clear() {
mItems.clear();
}
public void remove(int index) {
mItems.remove(index);
}
}
BaseAdapter 也是抽象函式,有兩個抽象函式需要子類實現,getLayout(),子類中直接return需要的layout 資源, getViewHolder 子類中return 需要的ViewHolder:
public abstract int getLayout();
protected abstract VH getViewHolder(View view) ;
Adapter 中定義了一個 ArrayList類 mItems,用於儲存資料,並公開了若干對 mItems 進行增刪的函式。
其他幾個函式也是實現一些初始化操作。
子類需要做的有,實現抽象函式,定義一個ViewHolder類,實現onBindTo函式。
BaseScrollPresenter 封裝
BaseScrollPresenter做的主要是把第一個網路請求封裝起來,先上程式碼:
package chenyu.jokes.base;
import android.os.Bundle;
import chenyu.jokes.app.AccountManager;
import java.util.ArrayList;
import nucleus.presenter.RxPresenter;
import rx.Observable;
import rx.functions.Action2;
import rx.functions.Func0;
import static rx.android.schedulers.AndroidSchedulers.mainThread;
import static rx.schedulers.Schedulers.io;
/**
* Created by chenyu on 2017/3/7.
*/
public abstract class BaseScrollPresenter<View extends BaseRxView, Model>
extends RxPresenter<View> {
protected int mPage;
private final int INIT_LOAD = 1;
@Override protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
restartableFirst(INIT_LOAD,
new Func0<Observable<ArrayList<Model>>>() {
@Override public Observable<ArrayList<Model>> call() {
return loadPageRequest()
.subscribeOn(io())
.observeOn(mainThread());
}
},
new Action2<View, ArrayList<Model>>() {
@Override public void call(View view,
ArrayList<Model> items) {
view.onItemsNext(items);
}
},
new Action2<View, Throwable>() {
@Override public void call(View view, Throwable throwable) {
view.onItemsError(throwable);
}
}
);
}
protected abstract Observable<ArrayList<Model>> loadPageRequest();
public void loadPage(int page) {
mPage = page;
start(INIT_LOAD);
}
}
父類是 RxPresenter,也是 Nucleus 框架的內容,是負載非同步處理資料請求的類,可以和 View 繫結。
兩個泛型 <View extends BaseRxView, Model>
,第一個View需要實現了 BaseRxView 介面,可以是BaseScrollActivity 或者 BaseScrollFragment,這就是定義 BaseRxView 的好處,否則就需要為aseScrollActivity 和 BaseScrollFragment 分別封裝一個 BasePresenter 類了。Model 是第一個網路請求需要的資料模型,也就是載入首頁,重新整理,上拉載入時用到的資料模型,如果對應的View還有其他網路請求,可以使用其他資料模型,在子類定義就行,與這個泛型無關。
有一個抽象函式子類必須實現,返回資料請求介面的資料,可能是網路請求,或者從本地資料庫獲取資料等,返回型別是 RxJava 的 Observable 泛型為 ArrayList<Model>
。
protected abstract Observable<ArrayList<Model>> loadPageRequest();
在 onCreate() 中用 restartableFirst() 函式註冊資料請求,這個是 RxJava 的形式,如果請求成功,則呼叫 View 的 onItemsNext() 函式,請求出錯則呼叫 onItemsError() 函式。
再看下 loadPage 函式,這個就是剛才在 View 中通過 getPresenter().loadPage(page)
來呼叫的那個,先給mPage賦值,再啟動請求。
public void loadPage(int page) {
mPage = page;
start(INIT_LOAD);
}
子類實現
介紹完了基類的封裝,接下來看下子類如何方便快捷地實現效果了。
View層:
@RequiresPresenter(FunPicPresenter.class)
public class FunPicFragment extends BaseScrollFragment<FunPicAdapter,FunPicPresenter, Data>{
@Override public FunPicAdapter getAdapter() {
return new FunPicAdapter();
}
@Override public int getLayout() {
return R.layout.fragment_fun_pic;
}
}
實現下getAdapter() 和 getLayout() 即可。
Adapter
public class FunPicAdapter extends BaseScrollAdapter<Data, FunPicAdapter.FunPicViewHolder> {
@Override public int getLayout() {
return R.layout.item_fun_pic;
}
@Override protected FunPicViewHolder getViewHolder(View view) {
return new FunPicViewHolder(view);
}
@Override public void onBindViewHolder(FunPicViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
holder.content.setText(mItems.get(position).getContent());
Uri uri = mItems.get(position).getUri(); Picasso.with(holder.itemView.getContext()).load(uri).into(holder.img);
}
public static class FunPicViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.content) public TextView content;
@BindView(R.id.img) public ImageView img;
public FunPicViewHolder(View view) {
super(view);
ButterKnife.bind(this, view);
}
}
}
定義一個內部類 ViewHolder,實現抽象函式getLayout() 和 getViewHolder() 函式,再實現下 UI 和資料的繫結關係即可。
Presenter層
public class FunPicPresenter extends BaseScrollPresenter<FunPicFragment, Data>{
@Override protected Observable<ArrayList<Data>> loadPageRequest() {
return App.getServerAPI().getFunPic(getSendToken(), mPage);
}
}
實現下loadPageRequest() 函式,返回網路請求結果就行。
這樣就完成了一個頁面。
以下是我的應用中的幾個列表頁面,都是用這個方式實現的,看看效果圖:
總結
使用我們封裝好的基類,子類只需要再實現兩三個函式,簡單的幾行程式碼,就可以實現列表頁面的下拉重新整理和上拉載入下一頁的功能了。不同的頁面,主要是要定義不同的 UI,以及UI和資料的關係,其他相同的處理都已經封裝到基類中,非常方便。