1. 程式人生 > >使用MVP+Retrofit+RxJava實現的的Android Demo (上)使用Nuclues庫實現MVP

使用MVP+Retrofit+RxJava實現的的Android Demo (上)使用Nuclues庫實現MVP

最近寫了一個 Android 小 Demo,使用基於Nucleus庫的MVP框架進行程式碼分割,並Retrofit和RxJava進行資料請求和處理,下面通過Demo程式碼分享下這幾種技術的使用方法。

需求

從網路Api獲取Json格式的笑話資料,通過列表方式顯示,列表分頁顯示,當上拉到最後一個數據是,自動從網路載入資料並顯示,在頂端進行下拉式重新整理資料。

MVP簡介

專案採用的是MVP框架,MVP分別代表

  • Model:資料接入層,例如資料庫API或者遠端伺服器API。
  • View:顯示資料並響應使用者操作。在Android系統上可以是Activity、Fragment、android.view.View或者對話方塊等。
  • Presenter:負責從Model提供資料給View的層,同時處理後臺任務。

傳統View-Model框架示意圖:
View-Model框架

MVP框架示意圖:
MVP框架

簡單對比傳統View-Model和MVP,可以發現MVP框架各個模組直接的耦合度更低,Presenter承接了Data(Model)和View直接的協調工作,Data和View是完全解耦的。

MVP帶來很多好處,例如:
- 使專案程式碼的層次劃分更加清晰,對測試和後期維護帶來便利
- 減小Activity的複雜度
- 更方便進行後臺服務生命週期控制,減少記憶體溢位的可能

MVP實踐

定義Model

我們的資料來自於網路API,返回的Json資料格式如下:

{
  "error_code": 0,
  "reason": "Success",
  "result": {
    "data": [
      {
        "content": "同事來我家打麻將,我特意燉了牛肉,心想著打完麻將,肉也燉好了,有吃有玩,其樂融融。但現實是,他們不但吃光了我的牛肉還贏光了我的錢。。。",
        "hashId": "323dea183f2baba507983829b55aeda1",
        "unixtime": 1490873030,
        "updatetime": "2017-03-30 19:23:50"
      
}, { "content": "花捲妹妹被包子給欺負了,花捲妹妹生氣的說道:“你等著我找我男朋友,他可是一個壯漢,打不死你。” 包子嘲笑道:“你去吧,我等著你!” 不一會兒,花捲妹妹帶了饅頭哥來到了包子面前,饅頭又大又壯。 可是包子上去三拳兩腳就把饅頭哥打敗了。 花捲妹妹哭道:“你這麼壯怎麼連他都打不過啊?” 饅頭哥委屈道:“你有所不知啊,我是酵母發酵的,我這不是壯,這是虛胖啊!”", "hashId": "9862276b1b74b4b7df8865e36eaa0349", "unixtime": 1490867030, "updatetime": "2017-03-30 17:43:50" } ]
}
}

依據返回資料格式的巢狀層次,定義了Response、Result、Data 3個Model,用於解析和儲存資料。
這裡用到了Jackson庫做Json資料處理,Parceler庫做Parcelable處理。使用方法都很簡單,用註解就可以。

package chenyu.jokes.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.parceler.Parcel;

/**
 * Created by chenyu on 2017/3/3.
 */

@Parcel @JsonIgnoreProperties(ignoreUnknown = true) public class Response {
  public Result result;
}
package chenyu.jokes.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import org.parceler.Parcel;

/**
 * Created by chenyu on 2017/3/7.
 */

@Parcel @JsonIgnoreProperties(ignoreUnknown = true) public class Result {
  public ArrayList<Data> data;
}
package chenyu.jokes.model;

import android.net.Uri;
import android.text.Spanned;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.parceler.Parcel;

import static chenyu.jokes.app.App.fromHtml;

/**
 * Created by chenyu on 2017/3/7.
 */

@Parcel @JsonIgnoreProperties(ignoreUnknown = true) public class Data {
  public String content;
  @JsonProperty("updatetime") public String time;
  public String hashId;
  public String url;

  public Uri getUri() {
    return Uri.parse(url);
  }

  public Spanned getContent() {
    return fromHtml(content);
  }

}

網路請求用的是Retrofit,主要程式碼如下,API介面中定義了getJokes()函式,通過get方式向伺服器API地址“http://119.23.13.228/content.php”傳送請求,函式返回值是RxJava裡的Observable,泛型為Model裡定義的Response。Retrofit和RxJava的使用這裡先不詳細介紹,具體會在下篇文章中介紹。

package chenyu.jokes.network;

import chenyu.jokes.model.Response;
import retrofit2.http.GET;
import retrofit2.http.Query;
import rx.Observable;

/**
 * Created by chenyu on 2017/3/3.
 */

public interface ServerAPI {
  String ENDPOINT = "http://119.23.13.228";

  @GET("/content.php") Observable<Response> getJokes(
      @Query("page") int page
  );
}

Nucleus

View層和Presenter層我們使用Nucleus庫。

Nucleus是一個很強大的Android MVP框架,支援將Presenter的狀態儲存到View/Fragment/Activity的state Bundle中,很方便處理請求成功和異常的情況,多個View可以繫結一個Presenter,只要一行註解就可以繫結Presenter,提供了豐富View層類NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity,支援自動重啟資料請求和取消RxJava訂閱。

View層處理:

我們用Fragment加RecyclerView來實現笑話列表,Fragment採用Nucleus中的NucleusSupportFragment類。
NucleusSupportFragment繼承自android.support.v4.app.Fragment,使用時新增對應的Presenter為泛型。

基類定義

考慮到專案其他頁面也會有這種列表形式,我們首先定義一個BaseScrollFragment,主要封裝列表顯示、上拉載入更多和下拉重新整理功能。BaseScrollFragment繼承自NucleusSupportFragment,指定兩個泛型,一個是RecyclerView的Adapter,另一個是要繫結的BaseScrollPresenter。
先上程式碼:

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.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import butterknife.BindView;
import butterknife.ButterKnife;
import chenyu.jokes.R;
import java.util.List;
import nucleus.view.NucleusSupportFragment;
/**
 * Created by chenyu on 2017/3/6.
 */

public  class BaseScrollFragment<Adapter extends BaseScrollAdapter,P extends BaseScrollPresenter> extends NucleusSupportFragment<P>{

  @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 Adapter mAdapter;

public void setAdapter(Adapter adapter) {
  mAdapter = adapter;
}

  public int getLayout(){
    return 0;
  }

@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);
    recyclerView.setAdapter(mAdapter);
    LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
    recyclerView.setLayoutManager(layoutManager);
  }

  ...
  refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
      @Override public void onRefresh() {
        mAdapter.clear();
        getPresenter().request(1);
        currentPage = 1;
        previousTotal = 0;
        refreshLayout.setRefreshing(false);
      }
    });

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        int visibleItemCount = recyclerView.getChildCount();
        int totalItemCount = recyclerView.getAdapter().getItemCount();
        int firstVisibleItem =( (LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();

        if(loading) {
          if(totalItemCount > previousTotal) {
            loading = false;
            previousTotal = totalItemCount;
          }
        }
        if(!loading && (totalItemCount - visibleItemCount) <= firstVisibleItem) {

          loading = true;
          currentPage ++;
          onLoadMore();
          previousTotal = totalItemCount;
        }
      }
    });
  }
  public void onItemsNext(List items) {
    mAdapter.addAll(items);
    mAdapter.notifyDataSetChanged();
    loading = false;
  }

  public void onItemsError(Throwable throwable) {
    Log.d("onItemError",throwable.getMessage());
  }

  public void onLoadMore(){
    getPresenter().request(currentPage);
  }

  @Override public void onDestroyView() {
    super.onDestroyView();
    mAdapter.clear();
  }
}

請求響應處理
Fragment中公開了兩個介面來更新UI,提供給Presenter在請求結束後使用。
onItemsNext()用於請求成功後更新介面卡,並通知RecyclerView顯示更新後的資料。
onItemsError()用於請求出錯後,提示使用者錯誤資訊。
Presenter中的具體呼叫在下文介紹。

  public void onItemsNext(List items) {
    mAdapter.addAll(items);
    mAdapter.notifyDataSetChanged();
    loading = false;
  }

  public void onItemsError(Throwable throwable) {
  Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_SHORT).show();
    Log.d("onItemError",throwable.getMessage());
  }

響應使用者操作
涉及到網路請求的使用者操作有下拉重新整理和上拉載入更多。
下拉重新整理通過重寫RecyclerView的addOnScrollListener()實現,在重新整理先通過getPresenter()獲取到當定的Presenter,再執行Presenter的request()方法,請求第一頁的資料。

getPresenter().request(1);

載入更多通過重寫RecyclerView的OnScrollListener()方法實現,當檢測到使用者上拉滑動到最後一個數據時,執行Presenter的request(),請求下一頁的資料。

public void onLoadMore(){
    getPresenter().request(currentPage);
  }
JokeFragment實現

基類BaseScrollFragment中已經實現了最基本的資料顯示等功能,下面再編寫JokeFragment繼承自BaseScrollFragment,用於實現具體的笑話列表顯示。

package chenyu.jokes.feature.Joke;

import android.os.Bundle;
import chenyu.jokes.R;
import chenyu.jokes.base.BaseScrollFragment;
import chenyu.jokes.presenter.JokePresenter;
import nucleus.factory.RequiresPresenter;

/**
 * Created by chenyu on 2017/3/6.
 */

@RequiresPresenter(JokePresenter.class)
public class JokeFragment extends BaseScrollFragment<JokeAdapter,JokePresenter> {

  public static JokeFragment create() {
    JokeFragment jokeFragment = new JokeFragment();
    return jokeFragment;
  }

  @Override public void onCreate(Bundle state){
    super.onCreate(state);
    setAdapter(new JokeAdapter());
  }

@Override public int getLayout() {
  return R.layout.fragment_joke;
}
}

JokeFragment的程式碼要少很多,首先在類名前添加註解,用來繫結JokePresenter類,Adapter和Presenter泛型指定為JokeAdapter和JokePresenter。

@RequiresPresenter(JokePresenter.class)
public class JokeFragment extends BaseScrollFragment<JokeAdapter,JokePresenter> {

在onCreate()時設定通過setAdapter(new JokeAdapter()); 設定RecyclerView的Adapter,通過getLayout()指定XML佈局檔案。

資料與UI的繫結在Adapter中進行,對Adapter類也封裝了BaseScrollAdapter類,由於和MVP關係不大,這裡就不詳細介紹了,具體可以看程式碼。

package chenyu.jokes.feature.Joke;

import android.widget.TextView;
import butterknife.BindView;
import chenyu.jokes.R;
import chenyu.jokes.base.BaseScrollAdapter;
import chenyu.jokes.model.Data;

/**
 * Created by chenyu on 2017/3/3.
 */

public class JokeAdapter extends BaseScrollAdapter<Data> {

  @BindView(R.id.content) public TextView content;
  @BindView(R.id.time) public TextView time;

 @Override public int getLayout() {
   return R.layout.item_joke;
 }

  @Override public void onBindViewHolder(ViewHolder holder, int position){
   super.onBindViewHolder(holder,position);
    content.setText(mItems.get(position).getContent());
    time.setText(mItems.get(position).time + " "+position);
  }

}

Presenter

基類和JokePresenter:
基類BaseScrollPresenter繼承自Nuclues中的RxPresenter,這是Nuclues提供支援RxJava的Presenter類。專案中Presenter程式碼如下:

package chenyu.jokes.base;

import nucleus.presenter.RxPresenter;

/**
 * Created by chenyu on 2017/3/7.
 */

public class BaseScrollPresenter<M> extends RxPresenter<M> {
  public void request(int page){

  }
}
package chenyu.jokes.presenter;

import android.os.Bundle;
import chenyu.jokes.app.App;
import chenyu.jokes.base.BaseScrollPresenter;
import chenyu.jokes.model.Response;
import chenyu.jokes.feature.Joke.JokeFragment;
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/3.
 */

public class JokePresenter extends BaseScrollPresenter<JokeFragment> {
  private int mPage = 1;
  public static final int GET_JOKES = 1;

  private Func1 errorCodeProcess = new Func1<Response, Observable<ArrayList<Data>>>() {
    @Override public Observable<ArrayList<Data>> call(Response response) {
      if(response.errorCode !=0) {
        return Observable.error(new Throwable(response.reason));
      }
      return Observable.just(response.result.data);
    }
  };

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

    restartableFirst(GET_JOKES,
        new Func0<Observable<ArrayList<Data>>>() {
          @Override public Observable<ArrayList<Data>> call() {
            return App.getServerAPI().getJokes(mPage).subscribeOn(io()).observeOn(mainThread())
                .flatMap(errorCodeProcess);
          }
        },
        new Action2<JokeFragment, ArrayList<Data>>() {
          @Override public void call(JokeFragment jokeFragment, ArrayList<Data> data) {
            jokeFragment.onItemsNext(data);
          }
        },
        new Action2<JokeFragment, Throwable>() {
          @Override public void call(JokeFragment jokeFragment, Throwable throwable) {
            jokeFragment.onItemsError(throwable);
          }
        }
    );
    request(1);
  }

  @Override  public void request(int page) {
    mPage = page;
    start(GET_JOKES);
  }

}

泛型JokeFragment表明Presenter要處理的View。
網路請求的註冊是在onCreate()的restartableFirst()方法中進行的,restartableFirst()可以包含3-4個引數。
第一個引數是int型,表示請求id,這裡我們使用整型常量GET_JOKES,值為1。

第二個引數是observableFactory,呼叫網路介面並返回Observable形式的資料結果。App.getServerAPI().getJokes(mPage).subscribeOn(io()).observeOn(mainThread()).flatMap(errorCodeProcess); 這一段鏈式RxJava程式碼,getJokes() 是之前定義的網路介面,subscribeOn(io()) 代表在UI執行緒進行網路請求,observeOn(mainThread()) 指明在主執行緒處理資料,更新UI,flatMap(errorCodeProcess) 是對返回的資料進行預處理,如果資料保護error資訊,則丟擲異常,否則就提取出Data資料交給後續處理。RxJava具體處理在下篇文章中介紹。

第三個引數是onNext回撥,會在請求成功後執行jokeFragment.onItemsNext(data);,通知View層進行資料更新。

第三個引數是onError回撥,可以為空,用於處理異常,會呼叫View層的異常處理方法jokeFragment.onItemsError(throwable);

註冊之後呼叫Presenter的start(int id)方法就可以啟動id對應的網路請求。
重寫BaseScrollPresenter中的request方法,設定要請求資料的頁碼mPage,並通過start(GET_JOKES); 啟動請求:

@Override public void request(int page) {
mPage = page;
start(GET_JOKES);
}

這個就是在Fragment下拉重新整理和上拉載入更多是呼叫的request方法。同時我們在Presenter的onCreate()中呼叫request(1),請求第一頁的資料,這樣在Fragment繫結Presenter啟動後就會獲取第一頁的資料並顯示。

至此,MVP相關程式碼就介紹完了,下篇文章將繼續介紹專案中Retrofit和RxJava的使用。