前言
最近使用MVP+Retrofit+Rxjava搭建了新项目的工程框架和网络请求回调框架。在网上也查看了一些资料,但是没有找到很贴合实际项目各自不同的请求及响应报文协议的解决方案。无奈,自己苦逼的一番摸索踩坑,终于搭建出了一个基本还算满意的解决方案,在这里和大家分享一下,希望能够帮助开发工程师们省去一些苦恼。
随着深入地开发,这个方案肯定还会陆续暴露出一些坑,我会一直维护这篇文章,将对应的坑和解决办法记录下来。决定使用我这个工程框架的同学还希望能够加学习小组QQ群: 193765960,将实际开发中遇到的坑在群中进行讨论和分享。
如果这篇文章对大家实际开发有所帮助,还望大家多多转发。
在阅读这篇文章之前要求读者对MVP,retrofit,rxjava,Gson具有一定的了解。
我在这里帮大家整理了一些比较好的文章,需要的同学自行查看。
《Retrofit使用教程(一)》
《Retrofit使用教程(二)》
《Retrofit使用教程(三) : Retrofit与RxJava初相逢》
《你真的会用Retrofit2吗?Retrofit2完全教程》
《你真的会用Gson吗?Gson使用指南》
《给 Android 开发者的 RxJava 详解》
版权归作者所有,如有转发,请注明文章出处:https://xiaodanchen.github.io/
1. MVP部分
本篇不讲解什么是MVP,不懂MVP的童鞋请自行百度。
1.1 基类
BasePresenter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public abstract class BasePresenter<T> { * View的引用,使用弱引用,当弱引用所引用的对象被销毁,软引用也会被释放 */ protected WeakReference<T> mViewRef; * Presenter与View关联 * @param view */ public void attachView(T view){ mViewRef = new WeakReference<T>(view); } * Presenter与View解除关联 */ public void detacheView(){ if(mViewRef != null){ mViewRef.clear(); mViewRef = null; } } protected T getView(){ if(mViewRef != null){ return mViewRef.get(); } return null; } * Presenter与View是否已关联 * @return */ public boolean isViewAttached(){ return mViewRef != null && mViewRef.get() != null; } }
|
.
BaseActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| * @param <V> 子activity的view接口 * @param <T> 子activity关联的presenter: T extends BasePresenter<V> */ public abstract class BaseActivity<V, T extends BasePresenter<V>> extends AppCompatActivity { protected T mPresenter; ...... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPresenter = createPresenter(); if(null != mPresenter){ mPresenter.attachView((V)this); } ...... } * 创建presenter * @return */ protected abstract T createPresenter(); @Override protected void onDestroy() { if(null != mPresenter){ mPresenter.detacheView(); mPresenter = null; } ...... super.onDestroy(); } ...... }
|
以上是我编写的两个基类,非本文重点,实际开发中大家也可以自己开发自己的基类。下面我将以登录界面做为业务类的demo讲解。由于项目原因,我会隐去部分细节,请谅解。
1.2 业务类:登录
在我的login业务文件夹下,我将要完成三个类的编写:LoginContract.java, LoginPresenter.java, LoginActivity.java
LoginContract.java
契约类,将约束定义MVP中V,P的接口
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface LoginContract { interface View extends IBaseView { void loginSucceed(User user); void loginFailed(String errMessage); } interface Presenter{ void login(String name, String pwd); } }
|
LoginPresenter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class LoginPresenter extends BasePresenter<LoginContract.View> implements LoginContract.Presenter { public LoginPresenter() { super(); } @Override public void login(String name, String pwd) { ...... } }
|
LoginActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class LoginActivity extends BaseActivity<LoginContract.View,LoginPresenter> implements LoginContract.View{ private EditText Edt_name; private EditText Edt_pwd; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); initData(); initView(); } @Override protected void initData() { } @Override protected void initView() { Edt_name = (EditText) findViewById(R.id.edt_name); Edt_pwd = (EditText) findViewById(R.id.edt_pwd); } @Override protected LoginPresenter createPresenter() { return new LoginPresenter(); } public void onLogin(View v){ mPresenter.login(Edt_name.getText().toString(),Edt_pwd.getText().toString()); } @Override public void loginSucceed(User user) { BaseFun.makeToast(getApplicationContext(), "登录成功!"+user.getNickName()); } @Override public void loginFailed(String errMessage) { BaseFun.makeToast(getApplicationContext(), errMessage); } }
|
好了,一个基本的MVP框架算是有了,Model的部分根据实际的业务可以灵活的去定义接口或业务类。
2. 网络通讯部分
网络框架采用的是retrofit+okhttp+gson+rxjava来进行搭建。在搭建的过程中,请注意各个库之间的版本问题,这个比较让人蛋疼。使用eclipse开发的读者也可以加学习小组QQ群: 193765960去下载我提供的jar包。
构建获取Retrofit对象
我封装了一个工具类RetrofitUtils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| * Retrofit工具类 */ public class RetrofitUtils { * 获取retrofit对象 * * retrofit对象默认使用我们自己的URL:DatasConfig.HOST * 如果想要指定具体的url,请调用build(baseUrl)方法 * 设置了Rxjava的请求回调机制 * 设置了Gson:json-Java bean的自动解析(序列化和反序列化) */ public static Retrofit build() { Retrofit retrofit = new Retrofit.Builder() .baseUrl(DatasConfig.HOST) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .client(RetrofitUtils.genericClient()) .build(); return retrofit; } * 获取retrofit对象 * * @param baseUrl * */ public static Retrofit build(String baseUrl) { Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()).client(RetrofitUtils.genericClient()) .build(); return retrofit; } * 定制client */ public static OkHttpClient genericClient() { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); Interceptor proheaders = new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request().newBuilder() .addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") .addHeader("Accept-Encoding", "gzip, deflate") .addHeader("Connection", "keep-alive") .addHeader("Accept","*/*") .addHeader("Cookie", "add cookies here") .build(); return chain.proceed(request); } }; OkHttpClient httpClient = new OkHttpClient.Builder() .addInterceptor(logging) .addInterceptor(proheaders) .readTimeout(60, TimeUnit.SECONDS) .connectTimeout(60, TimeUnit.SECONDS) .build(); return httpClient; } }
|
如代码,在开发中一定要加入日志logging功能,注意jar包的版本问题,你要是实在搞不定,就去加我的那个QQ群获取jar包吧。
定义登录业务的retrofit服务接口: LoginService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface LoginService{ @POST(DatasConfig.CMD_USER_LOGINYP) Call<BUResponse<User>> login(@Body BURequset requset); @POST(DatasConfig.CMD_USER_LOGINYP) Observable<BUResponse<User>> Rxlogin(@Body BURequset requset); @POST(DatasConfig.CMD_USER_LOGINYP) Observable<ResponseBody> RxJsonlogin(@Body BURequset requset); }
|
我在上面给大家实例了三种写法。
- 第一种不使用rxjava,返回的是Gson将响应报文反序列化后的User bean对象。
- 第二种使用rxjava,返回的是Gson将响应报文反序列化后的User bean对象。
- 第三种rxjava,返回的是okhttp的原始响应体ResponseBody,这样我们就可以获取原始的jsonString来自行解析。
以上都不是重点,从网上都可以找到很多文章。我要说的重点是我们可能需要根据实际的请求和响应的报文协议来自定义BUResponse和BURequset这两个类。这一块在网上实在是没有很多讲到这一块的文章,搞不懂这一块,整个的请求和响应流程就无法打通,估计开发者会哭死。
先来看一下示例的请求和响应的协议:
1 2 3 4
| { “head”:{”timestamp”:123456,}, “body”:{“name”:”abcd”,”pwd”:”123456”} }
|
BURequset.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class BURequset { private TreeMap<String, Object> head; private TreeMap<String, Object> body; public BURequset() { head = DemoApplication.reqHead; body = new TreeMap<String, Object>(); } public TreeMap<String, Object> getHead() { return head; } public void setHead(TreeMap<String, Object> head) { this.head = head; } public TreeMap<String, Object> getBody() { return body; } public void setBody(TreeMap<String, Object> body) { this.body = body; } }
|
如代码所示,为啥要用TreeMap?
其实我最开始的时候设计这个类,使用的是JsonObject,但是通过查看log发现,请求报文的jsonString多了一层name_value_pairs的封装。啥意思?即我们希望的json结构是
1 2 3 4
| { "xxxx":"zzzzzzz", "yyyy":"uuuuuuu" }
|
而实际生成的却是
1 2 3 4 5 6
| { "name_value_pairs": { "xxxx":"zzzzzzz", "yyyy":"uuuuuuu" } }
|
为啥会这样呢?原来是因为JsonObject的结构是这样的:
1 2 3 4 5
| public class JSONObject { ... private final Map<String, Object> nameValuePairs; ... }
|
而retrofit采用Gson做json的序列化时,会将JSONObject的nameValuePairs封装进序列化结果中。
那咋办?我后来采用了Map来封装我的head,body。最终得到了我想要的结果,自己也暗自得意。
可是,后来为啥要改成TreeMap呢?因为我在请我们公司的技术大牛帮我审查这套框架的时候,大牛给指了出来很多的坑。其中一个便是不能用Map,因为Map中的键值对是按照添加顺序排序的,而JsonObject是按照key的升序排序的,而我们常常会用报文来参与生成我们的校验sign。如果用Map的话,我同样的两个参数,put的顺序不同,得到的sign也不同,这显然是不对的。所以,最后采用了TreeMap来封装head和body,因为TreeMap是默认升序排序的。
大家如果要自己封装自己的请求和响应类的时候,一定要注意这个问题。
BUResponse.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class BUResponse<T> { private ResHead head; private T body; public ResHead getHead() { return head; } public void setHead(ResHead head) { this.head = head; } public T getBody() { return body; } public void setBody(T body) { this.body = body; } public static class ResHead{ private long timestamp; public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } } }
|
response采用泛型来设计实现通用(响应的结构都一样,特殊的响应结构就再单独处理吧)。
业务的调用和回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class LoginPresenter extends BasePresenter<LoginContract.View> implements LoginContract.Presenter { public LoginPresenter() { super(); } @Override public void login(String name, String pwd) { BURequset request = new BURequset(); request.getBody().put("pwd", pwd); request.getBody().put("name", name); LoginService service = RetrofitUtils.build().create(LoginService.class); service.Rxlogin(request) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<BUResponse<User>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { getView().loginFailed(e.toString()); } @Override public void onNext(BUResponse<User> result) { if (result != null) { ResHead head = result.getHead(); User user = result.getBody(); getView().loginSucceed(user); } } }); } }
|
3. Gson的基本使用
Gson实现Java Bean和JsonString之间的相互转化。但是现实往往是:
- java bean 和jsonString不是完全匹配的,换句话说,我们的java bean中有很多的属性是额外的。
- java bean的命名和报文的key是不匹配的,尤其是java bean在多个地方复用的时候。
不用担心,这些Gson都替你考虑到了,具体的请去参考《你真的会用Gson吗?Gson使用指南》
需要注意的坑:
- retrofit中使用gosn做java bean和json报文的转化时,入参和出参(请求和响应)中如果涉及同一类java bean对象,则要保持字段名的一致,java bean的属性名要和入参中的一致。
注意:
这篇文章中所用到的代码项目原因可能具有版权问题,大家是在实际开发中还是以参考借鉴为主吧。最后,如果这篇文章对大家有帮助,还是希望多多转发,让更多的朋友能够参与进这个框架的讨论中来,彼此受益。
3.1 利用Gson将List的jsonString 转为list对象
错误做法:
1 2 3 4 5
| String jsonString = PrefUtils.getString(mContext,key, ""); if(!"".equals(jsonString)){ Gson gson = new Gson(); List<Bean> vTypes = gson.fromJson(jsonString, List.class); }
|
正确做法:
1 2 3 4 5
| String jsonString = PrefUtils.getString(mContext,key, ""); if(!"".equals(jsonString)){ Gson gson = new Gson(); List<Bean> vTypes = gson.fromJson(jsonString, new TypeToken<List<Bean>>(){}.getType()); }
|
3.2 Gson 数据转换成json字符串时,默认会对一些特殊字符进行转义
这种情况下,如果服务器存在对Json数据的验证就会导致服务器端进行签名验证不会通过。
比如图片的base64数据,其末尾=\n处的=会被转义为\u003d,这时候如果对数据进行签名验证就会失败。
具体的字符串转义情况大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| REPLACEMENT_CHARS['"'] = "\\\""; REPLACEMENT_CHARS['\\'] = "\\\\"; REPLACEMENT_CHARS['\t'] = "\\t"; REPLACEMENT_CHARS['\b'] = "\\b"; REPLACEMENT_CHARS['\n'] = "\\n"; REPLACEMENT_CHARS['\r'] = "\\r"; REPLACEMENT_CHARS['\f'] = "\\f"; HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
|
解决这个问题的方法是:
1
| Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
3.3 利用Gson处理String类属性值为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static Gson bulidGson() { Gson gson = new GsonBuilder().serializeNulls() .registerTypeAdapterFactory(new NullStringToEmptyAdapterFactory()).disableHtmlEscaping().create(); return gson; } public static class NullStringToEmptyAdapterFactory<T> implements TypeAdapterFactory { public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { Class<T> rawType = (Class<T>) type.getRawType(); if (rawType != String.class) { return null; } return (TypeAdapter<T>) new StringNullAdapter(); } } public static class StringNullAdapter extends TypeAdapter<String> { @Override public String read(JsonReader reader) throws IOException { if (reader.peek() == JsonToken.NULL) { reader.nextNull(); return ""; } return reader.nextString(); } @Override public void write(JsonWriter writer, String value) throws IOException { if (value == null) { writer.value(""); return; } writer.value(value); } }
|
4. 需要考虑对异常和服务器错误的区分对待
IBaseView的接口要么定义onError(int level,String err);要么就分别定义onError和onException两个接口。
- 需要对异常做分类处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| private static final int UNAUTHORIZED = 401; private static final int FORBIDDEN = 403; private static final int NOT_FOUND = 404; private static final int REQUEST_TIMEOUT = 408; private static final int INTERNAL_SERVER_ERROR = 500; private static final int BAD_GATEWAY = 502; private static final int SERVICE_UNAVAILABLE = 503; private static final int GATEWAY_TIMEOUT = 504; * * 描述:对请求的异常进行逻辑分类 * @author xiaodanchen * @date 2016年10月12日 上午11:36:59 * QQ: 1404562848 * * @param e * @return */ public String processException(Throwable e){ String err; if (e instanceof HttpException){ HttpException httpException = (HttpException) e; switch(httpException.code()){ case UNAUTHORIZED: case FORBIDDEN: case NOT_FOUND: case REQUEST_TIMEOUT: case GATEWAY_TIMEOUT: case INTERNAL_SERVER_ERROR: case BAD_GATEWAY: case SERVICE_UNAVAILABLE: default: err = "网络异常: "+httpException.code(); break; } return err; }else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException){ err = "解析错误"; return err; }else if(e instanceof ConnectException){ err = "连接失败"; return err; }else { err = "未知错误"; return err; }
|
5. UI和任务回调设计
(这一块一定要在一开始就好好考虑,不然后期再改波及代码太大)
利用mvp+retrofit+rxjava:设计view的回调时,在一开始就要考虑好onNext,onError, onException, onComplete这几种任务状态下,返还给view的参数,及这几个view接口的入参结构:哪个任务?哪个请求?需要刷新哪个数据?需要刷新哪部分的界面?
- 进度条在什么时候取消:一个界面往往有多个任务执行并异步返回,怎么确认所有的任务都执行完了,在所有任务(指定任务)返回后取消界面的进度条?
- 单个任务怎么控制本地数据的局部更新,进而根据数据刷新界面?
- 当任务异常或错误时的回调