1. 程式人生 > >Android 基於 MVP 框架的下拉重新整理、上拉載入頁面,View和Presenter層基類封裝

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和資料的關係,其他相同的處理都已經封裝到基類中,非常方便。