Corelibs-master框架使用说明(一)

主体框架地址:Android架构探索(偶尔连接访问不出来)

介绍

  • 引入MVP模式.
  • 网络请求Retrofit.
  • 引入RxJava, RxAndroid.
  • 下拉刷新与自动加载Ultra-PullToRefresh.
  • 引入ButterKnife.
  • 引入RxBus事件驱动.
  • 其他常用工具库.
    新的模式最重要的就是引入了MVP以及Retrofit + Rxjava. 至于为何会引入这些模式与库后续会有篇幅一一介绍. 目前整个架构就像简介中描述的一样, MVP-based, RxJava, RxAndroid, Retrofit, Picasso, ButterKnife, RxBus, NineOldAndroids.

mvp

  • Model - 为应用程序提供数据, 如REST Api, db, SharedPreferences等.
  • View - 负责处理应用程序的UI显示以及响应用户事件并反馈处理结果. 在Android中通常是Activity或者是Fragment, 甚至可以是一个自定义View.
  • Presenter - 整个应用的控制中心, 控制着各种逻辑的分发. 例如View层接收到用户的点击事件, 将事件转发给Presenter, Presenter对事件一些简单的处理后转发给Model层, 并从Model层拿到处理结果并给View层显示.
mvp缺陷:
  1. 类的数量急剧增多. 每创建一个Activity需要创建与与其对应的View接口, Presenter, Model接口以及实现.
  2. MVP是一个方法论的东西,没有固定的实现方式.
  3. View持有Presenter引用, Presenter持有View和Model接口, 会导致多出许多初始化对象, 以及必要的销毁对象的重复代码.
  4. 额外的学习曲线
CoreLibs则专门对剩下两个问题做了优化处理:
  1. MVP是一个方法论的东西,没有固定的实现方式.
  2. View持有Presenter引用, Presenter持有View和Model接口, 会导致多出许多初始化对象, 以及必要的销毁对象的重复代码.
职责划分
1. 基本
  • View
    • 显示/隐藏加载框
    • UI与数据的绑定
    • 显示错误消息
    • 页面跳转
    • 转发请求至Presenter
    • 发送/监听RxBus事件
  • Presenter
    • 转发请求至Model
    • 检查用户输入是否合法
    • 控制View显示/因此加载框
    • 解析请求结果
    • 控制View显示错误消息
    • 控制View进行界面跳转
    • 控制登录状态
    • 从Model获取本地数据
    • 发送/监听RxBus事件
    • 尽量不要持有Context等与Framework相关的对象.
  • Model
    • 发送网络请求
    • File/SP/DB数据存取
2.上传图片
  • View
    • 压缩图片
    • 转发上传请求至Presenter
  • Presenter
    • 转发上传请求至Model
  • Model
    • 上传图片

CoreLibs设计

  • CoreLibs的设计初衷是为了解决新项目框架搭建比较耗时耗力的问题. 以前搭建一个新的框架需要从旧项目中拷贝通用的类以及各种资源文件, 是一件毫无难度的体力活. 因此将旧项目中可以通用的类与资源文件整合到一个依赖库中, 并且在做项目的过程中如果有新写的通用库或者修改的bug则直接在Corelibs中修改, 并且同步到一份副本中. 这样通过维护一份Corelibs副本来解决搭建新项目框架的问题.
  • 但是随着项目越来越多, 人员水平的参差不齐, 项目难度越来越大, 代码质量以及后期难以维护的问题就被放大了.这些问题主要是体现在于Activity/Fragment过大上, 当然还有一些其他因素影响. 因此新的CoreLibs引入了MVP来拆分Activity/Fragment的职责.
  • 引入MVP还有一个另外的原因就是模块的复用. 项目多就意味着有很多重复的功能 - 登录, 注册, 修改密码等等, 全部重新开发会浪费大量时间从而造成效率低下. 由于MVP完全将View与Presenter分离, Presenter持有的是View的接口, 因此, 拿登录模块举例, 只需要将登录的Presenter与View的接口复制到新项目中, 然后用新的Activity/Fragment去实现V的接口就完成了. 因为新项目和旧项目的登录的区别可能就只是在界面的不同, 内部逻辑基本上是完全一致的. MVP的被动视图很好的解决了逻辑变化不多, 但是界面变化频繁的情况.
  • 引入了MVP就势必会增加开发工作的复杂度, 毕竟一个类要拆分成好几个, 类之间的相互引用会增加一些重复代码. 因此CoreLibs通过一系列的Base类引入模板方法, 来达到减少一些重复代码的目的.
  • 同时, 引入一些第三方库来简化开发过程. 会在后面一一介绍.

View


界面

由于MVP被动视图的特性, Presenter会引用View的抽象以及Model的抽象, 同时View会引用Presenter, 那么此时几乎每个界面都会多出很多声明, 初始化的重复语句. 要消除这些重复可以考虑使用依赖注入的框架, 如Dagger, 但是考虑到Dagger本身的复杂性, 以及项目复杂度等原因, CoreLibs采用泛型及模板方法来消除重复.
基本上Android中是由Activity和Fragment来承载视图, 也有可能是View/ViewGroup. 因此在CoreLibs中提供了BaseActivity以及BaseFragment两个模板类, 来处理一些公共的视图逻辑. 至于View/ViewGroup的模板类会在以后加入. 模板类, 不仅可以简化代码, 同时也能起到一定的约束作用, 比如将onCreate方法拆分成多个符合单一职责原则的小方法, 从而避免onCreate过于臃肿.

抽象接口

被动视图中的View是将界面行为抽象出来的接口, 以供Presenter调用. 同Base模板类一样, View也可以抽象出几乎所有界面所共有的行为 -- BaseView. 比如只要有网络请求的界面(项目中几乎都是), 都会有加载框来提示用户当前正在加载网络数据, 加载完成后需要隐藏加载框, 并在适当时候提示用户一些错误消息. 一旦抽象出来BaseView, 就可以在Presenter等类里自动的调用这些公共的行为, 而无需再重复手动写.

Presenter


Presenter的共有逻辑一般就是与界面的绑定, 解绑等.

Model


Model这一层比较复杂, 随着项目的复杂度, Model可能会继续划分层级, 因此暂时没有抽出共有的逻辑.

View与Presenter的结合


View与Presenter之间是通过泛型来约束类型, 这里View以BaseActivity为例, BaseFragment与其类似.

BaseActivity声明
public abstract class BaseActivity<V extends BaseView, T extends BasePresenter<V>>
BasePresenter声明
public abstract class BasePresenter<T extends BaseView>
BaseView声明
public interface BaseView

可以看到, BaseActivity需要两个泛型, 第一个是继承自BaseView的接口, 第二个是继承自BasePresenter的Presenter, 同时Presenter里的泛型必须是V类型, 也就是第一个泛型类型.
BasePresenter则只需要一个继承自BaseView的接口.
这样做是为了约束View与Presenter的类型, 可以强制开发使用MVP模式, 同时可以将一些通用逻辑被抽象到BaseActivity与BasePresenter中:

BaseActivity部分通用逻辑
protected T presenter;

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

    presenter = createPresenter();
    if (presenter != null) presenter.attachView((V) this);

    init(savedInstanceState);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (presenter != null) presenter.detachView();
    presenter = null;
}

/**
 * 指定Activity需加载的布局ID, {@link BaseActivity}BaseActivity
 * 会通过{@link #setContentView}方法来加载布局
 *
 * @return 需加载的布局ID
 */
protected abstract int getLayoutId();

/**
 * 初始化方法, 类似OnCreate, 仅在此方法中做初始化操作, findView与事件绑定请使用ButterKnife
 */
protected abstract void init(Bundle savedInstanceState);

/**
 * 创建Presenter, 然后通过调用{@link #getPresenter()}来使用生成的Presenter
 * @return Presenter
 */
protected abstract T createPresenter();

上述代码声明了T类型的Presenter, 根据BaseActivity的声明可以得知T类型就是我们需要的Presenter的实际类型.
在onCreate方法中, 首先通过getLayoutId()抽象方法来获取Activity的布局id, 然后通过setContentView()设置到content中. 这样我们在BaseActivity的子类中就无需再复写onCreate方法. 然后通过createPresenter()抽象方法来实例化声明的presenter. 由于presenter是protected, 因此子类可以直接使用presenter成员变量. 接着调用presenter的attachView()方法将View与Presenter绑定起来, attachView()的实现会在下面贴出来. 最后调用了抽象方法init(Bundle savedInstanceState), 来替代onCreate方法.
在onDestroy中, 首先通过presenter的detachView()将View与Presenter解绑, 同时将presenter置空, 很大程度上避免了内存泄漏的可能.
可以看到, BaseActivity为子类做了如下逻辑, 子类无需再做:

  • 设置布局视图
  • 声明以及初始化Presenter
  • 将Presenter与View绑定
  • 初始化Activity
  • 将Presenter与View解除绑定

子类需要实现三个抽象方法, 具体含义请参考注释:

  • int getLayoutId()
  • T createPresenter()
  • void init(Bundle savedInstanceState)
BasePresenter部分通用逻辑
protected T view;

protected void onViewAttached() {}

/**
 * 将Presenter与MVPView绑定起来.
 * @param view MVPView
 */
public void attachView(T view) {
    this.view = view;
    onViewAttached();
}

/**
 * 将Presenter与MVPView解除.
 */
public void detachView() {
    view = null;
}

BasePresenter的逻辑比较简单, 就不做过多解释了, 同样的BasePresenter的子类可以直接使用view变量, 当然这里的view只是一个接口, 具体还需要BaseActivity子类去实现. 如果不去实现则会报类型转换错误. onViewAttached()方法通过名字也可以知道作用, 一些需要延迟初始化的变量或代码可以在覆写的onViewAttached()方法里写.

BaseView部分代码
/**
 * 加载时显示加载框
 */
void showLoading();

/**
 * 加载完成时隐藏加载框
 */
void hideLoading();

/**
 * 显示提示消息
 */
void showToastMessage(String message);

/**
 * 获取Context对象
 */
Context getViewContext();

BaseView接口中抽象了几乎所有页面都会使用的方法, 有了BaseView, 我们就可以利用BaseView为开发者做一些通用的逻辑, 比如自动隐藏加载框, 自动提示服务器返回的错误消息或者是网络错误消息等. 但是这段代码不在Presenter或View中, 以后会提及.
BaseActivty默认实现了BaseView, 因此BaseActivty的子类去实现BaseView的子类时, 只需要实现BaseView中没有的方法即可. 也可以通过覆写已实现的方法来达到细化控制的目的.

BaseActivity的完整声明
public abstract class BaseActivity<V extends BaseView, T extends BasePresenter<V>>extends FragmentActivity implements BaseView

以上就是通过泛型来抽象通用逻辑的过程, 当然这三个Base类中还可以加入更多的逻辑, 这里只是其中一部分, 比如BaseActivity中还加入了ButterKnife的bind()方法, 以及加载框等.

创建新的Activity

如何使用BaseActivity来创建一个新的Activity, 并且遵循MVP设计模式?

抽象

首先需要了解清楚需求, 确认页面有哪些行为, 抽象出View的接口以及Model的接口. 下面以登录界面为例. 一个基本登录界面应该包含用户名和密码输入框, 登录按钮, 以及注册和忘记密码的入口. 那么我们应该如何抽象出View呢? 从登录流程入手(从程序的角度出发):

  1. Activity检测到用户点击了登录按钮
  2. Activity转发事件到Presenter
  3. Presenter简单验证用户名和密码是否合法
    3.1. Presenter通知View用户名或者密码不合法
  4. Presenter通知View显示加载框
  5. Presenter调用Model的登录接口
  6. Model返回登录结果给Presenter
  7. Presenter解析结果
    7.1. Presenter通知View隐藏加载框
    7.2. Presenter通知View登录成功
    7.3. Presenter通知View错误消息

既然我们使用的是MVP被动视图, 那么我们应该抽象的是View的被动行为, 即Presenter告诉View要做的行为. 从上述流程我们可以提取出这么几个行为: 提示用户名不合法, 提示密码不合法, 显示加载框, 隐藏加载框, 处理登录成功, 显示登录接口返回的错误. 但是BaseView已经包含了显示加载框, 隐藏加载框以及显示一些错误消息, 那么对应到代码则应该是这样:

public interface LoginView extends BaseView {
    void loginComplete();
    void usernameError();
    void passwordError();
}

Presenter里的公共方法以及Model中的接口:

public class LoginPresenter extends BasePresenter<LoginView> {
  LoginApi api;
  @Override
  public void onStart() {
  }
  @Override
  protected void onViewAttach() {
      super.onViewAttach();
      api = ApiFactory.getFactory().create(LoginApi.class);
  }
  public void login(String username, String password){
      if (!checkUserInput(username, password)) return;
      LoginRequestBean bean = new LoginRequestBean();
      bean.username=username;
      bean.password=password;
      view.showLoading();
      api.login(bean)
            .compose(new ResponseTransformer<>(this.<BaseData> bindLifeCycle()))
            .subscribe(new ResponseSubscriber<BaseData>() {
                @Override public void success(BaseData baseData) {
                    // 此处意味着登录成功, 其他错误情况已自动处理
                    view.loginComplete();
                }
            });
  }
  private boolean checkUserInput(String username, String     password) {
      if (!ValidationUtil.validatePhone(username)) {
          view.usernameError();
          return false;
      }
      if (stringIsNull(password) || password.length() < 6 ||     password.length() > 16) {
          view.passwordError();
          return false;
      }
      return true;
  }
}

Presenter的代码比较简单, 主要就是做了一次用户名和密码的验证, 然后发送网络请求, 需要注意的是发送网络请求的代码使用了Retrofit2与RxJava. 隐藏加载框, 错误提示等操作都在ResponseSubscriber类里面做了, 后续会提到这一块.

Model里则是直接使用的Retrofit2, 这里就不赘述了.

public interface LoginApi {
  //login
  @POST(Urls.LOGIN)
  Observable<BaseData<UserBean>> login(@Body LoginRequestBean bean);
}
实现
public class LoginActivity extends BaseActivity<LoginView, LoginPresenter>
    implements LoginView {
    @Override
    protected int getLayoutId() {
        return R.layout.activity_login;
    }
    @Override
    protected void init(Bundle savedInstanceState) {
        initView(); 
    }
    @Override
    protected LoginPresenter createPresenter() {
        return new LoginPresenter();
    }
    @Override
    public void loginComplete() {
        startActivity(MainActivity.getLaunchIntent(this)); //跳转        至主页
        finish();
    }
    @Override
    public void usernameError() {
        showToast("请输入正确的手机号"); //建议使用string资源
    }
    @Override
    public void passwordError() {
        showToast("请输入6-16位密码"); //建议使用string资源
    }
/**
   *在这里建议每个Activity都提供以此方法:
   *好处是其他界面里不用去组装intent, 也不必在意intent里extra的名字, 特别是很多界面需要跳至这个界面的时候,
   * 能减少很多重复代码, EXTRA这种静态常量也只需要在一个类的内部维护就可以了.
   *在其他界面startActivity(LoginActivity.getLaunchIntent(this, 1));
   */
    public static Intent getLaunchIntent(Context context, int     flag) {
        Intent intent = new Intent(context, LoginActivity.class);
        intent.putExtra(flag, EXTRA_FLAG);
        return intent;
    }
总结

创建一个新Activity或者Fragment可以遵循以下步骤:

  • 分析需求! 重要
  • 抽象View, Model
  • 创建Presenter, 填入View的泛型并提供公共方法
  • 创建新的Activity并继承自BaseActivity
  • 填入相应的View与Presenter的泛型
  • 实现BaseActivity的三个抽象方法
  • 实现先前抽象出来的View接口

如果一个界面没有什么逻辑只是单纯的显示应该怎么做?

  • 创建新的Activity并继承自BaseActivity
  • 实现BaseActivity的三个抽象方法并在createPresenter()方法中返回null

单纯的继承BaseActivity与继承其他Activity一样并无太大区别, 只需要实现三个抽象方法即可, 其他用法都一样. 但是如果一旦使用了presenter变量则会抛出空指针异常.

一键生成MVP相关类 由于引入了MVP模式, 会较从前多了许多类, 典型的就是基本上每一个页面都有与之对应的View和Presenter, 至少会多两个类, 并且还需要使用泛型将三者关联起来. 懒癌晚期患者可能会很不适应, 我自己呢, 也觉得太麻烦, 因此花了一天时间开发了一个简单的IntelliJ/Android Studio插件 - MvpClassesGenerator来一键生成MVP的相关类.

发送网络请求

APP的联网能力现在必不可少, 所以选择一个适合的网络框架至关重要. 我主要以以下几个方面考虑:

  • 使用简单简洁
  • 方便的自定义, 如加请求头
  • 方便的取消请求
  • 能方便的与GSON等序列化库结合
  • 完善的Log信息打印

Retrofit2

最后选择了Retrofit2, 具体介绍可以参考 用 Retrofit 2 简化 HTTP 请求. 基本用法可以参考 官方网站.

这里就不再赘述Retrofit2的优缺点, 主要谈谈引入Retrofit2后需要做的一些准备工作:

  1. 将Retrofit2的API应用至Model层
  2. 与GSON结合
  3. 自定义Log输出
  4. 处理一些通用的异常
  5. 取消请求
  6. 预处理请求结果

1. 将Retrofit2的API应用至Model层

Retrofit的特色是使用注解与接口来描述网络请求, 在多数情况下能很好的与Model层的接口结合起来, 而无需重写实现类.

public interface HomepageApi {
  @POST(Urls.GET_TYPES)
  Call<BaseData> getTypes();

  @FormUrlEncoded
  @POST(Urls.GET_HOT_PRODUCTS)
  Call<BaseData> getTopProducts(@Field("pageNo") int pageNo, 
                    @Field("pageSize") int pageSize);
}


Retrofit retrofit = new Retrofit.Builder()
  .baseUrl(Urls.BaseUrl)
  .build();

HomepageApi api = retrofit.create(HomepageApi.class);

然后通过api对象去调用getTypes或getTopProducts接口. 通过这种方式我们就能直接将描述请求的接口应当做一个Model来使用. 但是每次使用都去要生成一个Retrofit.Builder, 然后做一些配置, 额外增加了重复代码, 因此我们可以写一个工具类, 来专门生成Retrofit接口实例

在这里我使用了两个类:

  1. RetrofitFactory 持有一个Retrofit单例, 专门用于配置Retrofit, 比如baseUrl, converter, adapter之类.
  2. ApiFactory 单例, 持有一个HashMap对象, 用于缓存Retrofit生成的接口实例, 而无需重复生成.
/**
 *用于获取配置好的retrofit对象, 通过设置{@link 
  Configuration#enableLoggingNetworkParams()}来启用网 
  络请求
 * 参数与相应结果.
 */
public class RetrofitFactory {
  private static Retrofit retrofit;
  private static String baseUrl;

  public static void setBaseUrl(String url) {
      baseUrl = url;
  }  

  /**
   * 获取配置好的retrofit对象来生产Api对象
   */
  public static Retrofit getRetrofit() {
      if (retrofit == null) {
          if (baseUrl == null || baseUrl.length() <= 0)
              throw new IllegalStateException("请在调用getFactory之前先调用setBaseUrl");

          Retrofit.Builder builder = new Retrofit.Builder();

          builder.baseUrl(baseUrl)
                  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())   // 参考RxJava
                  .addConverterFactory(GsonConverterFactory.create()); // 参  考与GSON的结合
          // 参考自定义Log输出
           if (Configuration.isShowNetworkParams()) {
              OkHttpClient client = new OkHttpClient.Builder().addInterceptor(
                      new HttpLoggingInterceptor()).build();
              builder.client(client);
          }
          retrofit = builder.build();
      }
      return retrofit;
    }
}


/**
 *通过定义好的api接口以及Retrofit来生成具体的实例.
 */
public class ApiFactory {
    private static ApiFactory factory;
    private static HashMap<String, Object> serviceMap = new HashMap<>();

    public static ApiFactory getFactory() {
        if (factory == null) {
            synchronized (ApiFactory.class) {
                if (factory == null)
                    factory = new ApiFactory();
            }
        }
        return factory;
    }

    public <T> T create(Class<T> clz) {
        Object service = serviceMap.get(clz.getName());
        if (service == null) {
            service = RetrofitFactory.getRetrofit().create(clz);
            serviceMap.put(clz.getName(), service);
        }
        return (T) service;
    }
}


HomepageApi api = ApiFactory.getFactory().create(HomepageApi.class);

在使用前还需要调用

RetrofitFactory.setBaseUrl(Urls.ROOT);

2. 与Gson结合

Retrofit2可以很方便的通过Converter与GSON结合, 在RetrofitFactory类中已经做了绑定, 无需再处理.

builder.addConverterFactory(GsonConverterFactory.create());

我们只需要在定义接口的时候给定结果类型, Retrofit内部会调用GSON解析JSON数据, 并转换成相应的实体类.

Call<BaseData> getTypes();

3. 自定义Log输出

为何要有Log输出? 有了网络请求的Log我们就能很方便的追踪问题, 看是服务器返回的数据有误, 还是我们解析有误. Retrofit2内部使用的是okhttp3, 因此可以考虑使用okhttp的Interceptor来打印Log输出:

/**
 *OkHttp的{@link Interceptor}, 通过设置
 * {@link Configuration#enableLoggingNetworkParams()}打    印网络请求参数与响应结果
 */
public class HttpLoggingInterceptor implements Interceptor {
    private static final String TAG = "HttpLogging";

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request(); // 获取请求
        long t1 = System.nanoTime();

        Buffer buffer = new Buffer();
        request.body().writeTo(buffer); //获取请求体

        Log.e(TAG, String.format("Sending request %s on %s%n%sRequest Params: %s",
                request.url(), chain.connection(), request.headers(), buffer.clone().readUtf8()));
        buffer.close();

        Response response = chain.proceed(request); //执行请求
        long t2 = System.nanoTime();

        BufferedSource source = response.body().source(); //获取请求结果
        source.request(Long.MAX_VALUE);
        buffer = source.buffer().clone(); //克隆返回结果, 因为buffer只能使用一次
        Log.e(TAG, String.format("Received response for %s in %.1fms%n%sResponse Json: %s",
                response.request().url(), (t2 - t1) / 1e6d, response.headers(),
                buffer.readUtf8()));
        return response;
    }
}

然后在RetrofitFactory中加入:

if (Configuration.isShowNetworkParams()) {
    OkHttpClient client = new OkHttpClient.Builder().addInterceptor(
        new HttpLoggingInterceptor()).build();
    builder.client(client);
}

以下是Log格式:

HttpLogging: Sending request http://xx.xx.xx.xx:xxxx/xxx/app/findAttractions.htm on null
     Request Params: city=%E6%AD%A6%E6%B1%89&pageNo=1&pageSize=10&latitude=xxx&longitude=xxx
HttpLogging: Received response for http://xx.xx.xx.xx:xxxx/xxx/app/findAttractions.htm in 181.2ms
     Server: Apache-Coyote/1.1
     Content-Type: text/html;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Wed, 23 Mar 2016 06:50:48 GMT
     OkHttp-Sent-Millis: 1458715847034
     OkHttp-Received-Millis: 1458715847128
     Response Json: {"data":{"attractionsList":[{"distance":13110.6,"id":13,"image":"xx.xx.xx.xx:xxxx/xxx/upload/201603171607138491.jpg","latitude":30.559024,"longitude":114.30341,"name":"黄鹤楼","profile":null,"score":5,"voice":"xx.xx.xx.xx:xxxx/xxx/upload/201603171009165109.mp3"}]},"msg":"成功","page":{"pageCount":1,"pageNo":1,"pageSize":10,"totalCount":1},"status":1}

RxJava

RxJava近来在Android领域非常火爆, 那么RxJava到底是什么呢? 这里不会过多的解释, 请参考文章 给 Android 开发者的 RxJava 详解深入浅出RxJava系列 .

RxJava在CoreLibs中有何应用? 最主要的就是与Retrofit2结合, 大大简化了调用网络接口的代码.

  1. 网络请求异常处理
  2. 预处理请求结果
  3. 自动取消网络请求
  4. 对请求结果做各种变换

注意: 下列功能均需要新建一个实体类, 如BaseData, 并实现ResponseHandler.IBaseData接口.
RxJava可以与Retrofit2无缝相连, 只需要在Retrofit.Builder的addCallAdapterFactory()方法中传入RxJavaCallAdapterFactory.create()即可. RetrofitFactory类中已经做了相应的处理.
接口声明方式则改为如下:

@POST(Urls.GET_ADS)
Observable<BaseData> getAds();

这样就可以直接对getAds方法返回的Observable对象做操作.

以下内容均是建立在了解RxJava的Observable, Subscribe, Subscriber等概念, 并熟悉写法的基础上.


1. 网络请求异常处理

在RxJava中, 一个事件序列中如果出现异常就会回调onError, 我们可以利用这个特性来做统一的异常处理. 如果出现非业务方面的异常, 如网络连接失败, 数据解析失败, 服务器异常等都会进入Subscriber的onError(Throwable e), 通过判断Throwable的类型可以确定异常的具体原因并提示用户:

@Override
public void onError(Throwable e) {
    if (e instanceof ConnectException) {
      // 网络连接异常
    } else if (e instanceof HttpException) {
      // 服务器异常
    } else if (e instanceof SocketTimeoutException) {
      // 连接超时
    } else {
      // 其他异常, 如GSON解析错误等
    }
}

异常种类正在不断完善中, 肯定是不止这几种, 但是这是目前最常见的几种异常. 我们可以新建一个类继承自RxJava的Subscriber, 然后在订阅网络请求返回的Observable时使用此类, 而不是默认的Subscriber:
public abstract class ResponseSubscriber<T> extends Subscriber<T> {

private BaseView view;

public ResponseSubscriber() {}

public ResponseSubscriber(BaseView view) {
    this.view = view;
}

@Override
public void onCompleted() {
  view = null;
}

@Override
public void onError(Throwable e) {
  if (view != null) {
    if (e instanceof ConnectException) {
      view.showToastMessage(view.getViewContext().getString(R.string.network_error));
    } else if (e instanceof HttpException) {
      view.showToastMessage(view.getViewContext().getString(R.string.network_server_error));
    } else if (e instanceof SocketTimeoutException) {
      view.showToastMessage(view.getViewContext().getString(R.string.network_timeout));
    } else {
      view.showToastMessage(view.getViewContext().getString(R.string.network_other));
    }
  }
  view = null;
}

@Override
public void onNext(T t) {
  view = null;
}

}
我们可以利用抽象出来的BaseView自动提示用户, 而无需每一个请求都做判断. 需要注意的是, 在onNext, onError, onCompleted中需要将通过构造函数传入的BaseView置空. ResponseSubscriber已经实现了onNext, onError, onComplete这样抽象方法, 因此在其实例中可以没有任何实现方法, 也可以选择性的覆写, 如:

api.getTypes()
  .subscribe(new ResponseSubscriber() {
    @Override
    public void onNext(BaseData data) {
      super.onNext(data);
    }
  });

2.预处理请求结果

进一步的, 我们可以定义自己的方法, 让实例选择覆写, 给予更大的灵活性:

/**
*请求成功同时业务成功的情况下会调用此函数
*/
public abstract void success(T t);
/**
* 请求成功但业务失败的情况下会调用此函数.
*/
public boolean operationError(T t, int status, String message)     {}
/**
* 请求失败的情况下会调用此函数
*/
public boolean error(Throwable e) {}

由于我们Server端返回的JSON都有固定的格式, 因此所有的返回结果都会以BaseData实体来接收:

public class BaseData {
    public int status; // 操作结果, 1为成功, 其他为失败
    public String msg; // 返回的消息
    public MapData data; // 携带的数据
    public Page page; // 分页数据
}

只要status不为1, 就意味着业务失败, 因此抽象出了operationError函数. 当status不为1的情况下, 会调用operationError. 一般情况下, 业务失败我们都需要将服务器返回的消息展示给用户, 我们可以同样的, 将此逻辑写在ResponseSubscriber中.

public void onNext(T t) {
   resetLoadingStatus();
   BaseData data;
   if (t instanceof BaseData) {
     data = (BaseData) t;
     if (data.status == SUCCESS_STATUS) {
       success(t);
     } else {
       if (!operationError(t, data.status, data.msg)) {
         handleOperationError(data.msg);
       }
     }
   } else {
     success(t);
   }
   release();
}

public void handleOperationError(String message) {
   if (view != null)
     view.showToastMessage(message);
}

public void resetLoadingStatus() {
   if (view != null)
     view.hideLoading();
}

public void release() {
   view = null;
}

不论业务是否成功, 都是请求成功, 因此需要在onNext书写判断逻辑. 上述代码很简单, 主要流程是首先重置加载框状态, 开发人员就无需每次都去隐藏加载框. 然后判断返回结果是不是BaseData, 如果不是则直接调用success, 让开发人员自行处理. 如果是BaseData则判断status是否是1, 是1就调用success, 不是1就调用operationError(t, data.status, data.msg), 根据返回结果判断是否调用handleOperationError.

onError中的逻辑类似:

public void onError(Throwable e) {
   resetLoadingStatus();
   e.printStackTrace();
   if (!handler.error(e)) {
      handleException(e);
   }
   release();
}

接下来再重新看看自定义的三个函数:

    /**
     * 请求成功同时业务成功的情况下会调用此函数
     */
    void success(T t);

    /**
     * 请求成功但业务失败的情况下会调用此函数.
     * @return 是否需要自行处理业务错误.
     * true - 需要, 父类不会处理错误
     * false - 不需要, 交由父类处理
     */
    boolean operationError(T t, int status, String message);

    /**
     * 请求失败的情况下会调用此函数
     * @return 是否需要自行处理系统错误.
     * true - 需要, 父类不会处理错误
     * false - 不需要, 交由父类处理
     */
    boolean error(Throwable e);

总结一下, 使用ResponseSubscriber去订阅网络请求结果时, 可以选择不传入BaseView, 这样所有判断逻辑都需要自行实现. 如果传入BaseView, 默认情况会实现所有逻辑. success方法必须覆写, 可以选择覆写operationError与error, 如果覆写返回true, 则意味已经自行处理逻辑, ResponseSubscriber不会再去处理, 反之则会处理. 一般情况下如下写法就够了:

api.getTypes()
  .subscribe(new ResponseSubscriber<BaseData>(view) {
    @Override
    public void success(BaseData baseData) {
      if (baseData.data != null && baseData.data.types != null)
        view.renderTypes(baseData.data.types); // view在BasePresenter中声明并实例化
    }
  });

以上就是预处理请求结果.


3. 自动取消网络请求

我们都知道要在Activity/Fragment的onDestory中取消正在连接的网络请求, 避免内存泄漏或其他风险, 提高体验. 那么在Retrofit2+RxJava中怎么取消请求呢? 如果是这么定义的请求:
不过多介绍了,点我

RxAndroid

RxAndroid还是Jake Wharton大神写的针对Android平台的RxJava扩展, RxAndroid可以很方便的使用AndroidSchedulers.mainThread()将数据发送到Android的主线程, 也可以替代一些诸如点击回调的事件等等等. 具体用途可以自行百度.
这里我选取了RxAndroid lifecycle来替代Subscription. RxAndroid lifecycle可以将给定的Observable绑定至Activity/Fragment的生命周期. 要使用RxAndroid lifecycle首先需要使Activity/Fragment继承自RxFragmentActivity/RxFragment.因此CoreLibs中的BaseActivity与BaseFragment均是继承自RxFragmentActivity/RxFragment.
然后就可以使用Observable的compose方法, 以及RxFragmentActivity/RxFragment的bindToLifecycle()/bindUntilEvent(ActivityEvent event).

最后演变成完整正确的代码如下:演变过程,点我

api.getAds().compose(new ResponseTransformer<>(this.<BaseData> bindToLifeCycle()))
            .subscribe(new ResponseSubscriber<BaseData>(view) {
                @Override
                public void success(BaseData baseData) {
                    if (baseData.data.ads != null)
                        view.renderAds(baseData.data.ads);
                }
            });

以上代码自动处理了:

  1. 在合适的时候取消请求
  2. 在io线程发送请求, 在Main线程接受结果
  3. 将结果转换为BaseData类型
  4. 判断业务是否执行成功, 失败则提示服务器返回的消息
  5. 识别错误类型, 并做相应的提示.
  6. 打印相应的请求Log

4. 对请求结果做各种变换

想象一下, 如果一个页面有两个请求, 一个获取所有的一级分类, 另一个根据第一个一级分类的id去获取二级分类. 一般我们会在第一个网络请求成功后, 去解析数据并发送第二个网络请求. 但是这么写会嵌套, 如果解析代码很多会难以阅读, 这时候我们可以借助Observable的flatMap方法解决这个问题:

final List<Category> categories = new ArrayList<>();
    api.getCategories()
            .flatMap(new ResponseAction<BaseData, Observable<BaseData>>(view) {
                @Override 
                public Observable<BaseData> onCall(BaseData baseData) {
                    if (baseData.data != null && baseData.data.categories != null) {
                        categories.addAll(baseData.data.categories);
                        return api.getSubAttractions(baseData.data.categories.get(0).id);
                    }
                    return null;
                }
            }).compose(new ResponseTransformer<>(this.<BaseData> bindLifeCycle()))
            .subscribe(new ResponseSubscriber<BaseData>(view) {
                @Override public void success(BaseData baseData) {
                    view.renderCategories(categories);
                    if (baseData.data != null && baseData.data.subCategories != null)
                        view.renderSubCategories(baseData.data.subCategories);
                }
            });

flatMap中需要传入一个Func1对象, 在这种情况下, Action里的数据也是需要解析的, 因此也可以创建一个ResponseAction类用于解析结果与错误. 代码与ResponseSubscriber类似, 就不贴出来了. 但是此时就有两个类似的类, 大部分代码都一样. 违反了DRY原则, 如果一旦数据结构有变或者异常类型增多则需要修改两个类, 因此将部分共同的代码, 提取到一个新的处理类中。
ResponseSubscriber与ResponseAction则均通过ResponseHandler处理. ResponseSubscriber与ResponseAction的思路相同, 但是具体实现又有差别, 这里不再赘述.

最后

RxJava最核心的功能就是对数据做各种变换, 在此基础之上又衍生出各种用法. 可以参考 可能是东半球最全的RxJava使用场景小结 .

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容