使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP

最近写了一个 Android 小 Demo,使用基于Nucleus库的MVP框架进行代码分割,并Retrofit和RxJava进行数据请求和处理,下面通过Demo代码分享下这几种技术的使用方法。

需求

从网络Api获取Json格式的笑话数据,通过列表方式显示,列表分页显示,当上拉到最后一个数据是,自动从网络加载数据并显示,在顶端进行下拉式刷新数据。

最终效果图:
使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP_第1张图片
App下载地址: http://a.app.qq.com/o/simple.jsp?pkgname=chenyu.jokes
微信扫描下载APP:
使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP_第2张图片
源码地址: https://github.com/zhongchenyu/jokes
由于后续代码可能会做重构,本文介绍的代码保存在 demo1 分支,请 checkout。

MVP简介

项目采用的是MVP框架,MVP分别代表

  • Model:数据接入层,例如数据库API或者远程服务器API。
  • View:显示数据并响应用户操作。在Android系统上可以是Activity、Fragment、android.view.View或者对话框等。
  • Presenter:负责从Model提供数据给View的层,同时处理后台任务。

传统View-Model框架示意图:
使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP_第3张图片

MVP框架示意图:
使用MVP+Retrofit+RxJava实现的的Android Demo (上)使用Nuclues库实现MVP_第4张图片

简单对比传统View-Model和MVP,可以发现MVP框架各个模块直接的耦合度更低,Presenter承接了Data(Model)和View直接的协调工作,Data和View是完全解耦的。

MVP带来很多好处,例如:
- 使项目代码的层次划分更加清晰,对测试和后期维护带来便利
- 减小Activity的复杂度
- 更方便进行后台服务生命周期控制,减少内存溢出的可能

了解更多,参考MVP介绍文章: Introduction to Model View Presenter on Android

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;
}
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 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>>() {
    @Override public Observable> 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>>() {
          @Override public Observable> call() {
            return App.getServerAPI().getJokes(mPage).subscribeOn(io()).observeOn(mainThread())
                .flatMap(errorCodeProcess);
          }
        },
        new Action2>() {
          @Override public void call(JokeFragment jokeFragment, ArrayList data) {
            jokeFragment.onItemsNext(data);
          }
        },
        new Action2() {
          @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的使用。

你可能感兴趣的:(Android)