Retrofit :A type-safe HTTP client for Android and Java .
retrofit : noun. a component or accessory added to something after it has been manufactured
 
mocky.io: Mock your HTTP responses to test your REST API    
 
前言 本文默认ConverterFactory为GsonConverterFactory。  
请求阶段
Header的统一处理   
访问绝对路径   
Map的使用避免声明冗余的类   
RequestBody为String 及 文件上传   
 
返回阶段  
后台Json空数据规范   
空数据Void声明   
ResponseBody为String   
ResponseBody的多次读取   
统一的错误处理   
 
 
使用Interceptor可为每一个request添加统一的Header信息   
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 public class OkHttpInterceptor implements okhttp3.Interceptor {       @Override       public Response intercept(Chain chain) throws IOException {           Request originalRequest = chain.request();              Request newRequest = originalRequest.newBuilder()                   .header("sdkInt", Integer.toString(Build.VERSION.SDK_INT))                   .header("device", Build.DEVICE)                   .header("user-agent", "android.sodino")                   .header("ticket", Constant.TICKET)                   .header("os", "android")                   .header("version", "2.6")                   .header("Content-Type", "application/json")                   .build();              Response response = chain.proceed(newRequest);           return response;       }   }               public class RetrofitUtil {          // 定义统一的HttpClient并添加拦截器       private static OkHttpClient okHttpClient = new OkHttpClient.Builder()                               .addInterceptor(new OkHttpInterceptor()).build();          public static Retrofit getComonRetrofit(String baseUrl) {           Retrofit retrofit = new Retrofit.Builder()                   .baseUrl(baseUrl)                   .addConverterFactory(GsonConverterFactory.create())                   .callbackExecutor(ThreadPool.getThreadsExecutor())                   .client(okHttpClient)                   .build();              return retrofit;       }   } 
 
 
访问绝对路径 
@Url @URL  resolved against the base URL. 
 
这种方式是动态的灵活的,不需要提前预知的。  
1 2 @GET   Call<ResponseBody> list(@Url String url); 
 
endpoint 为绝对路径   
 
这种方式需要在编码时提前预知,与baseUrl的理念是相冲突的,不推荐使用这种方式。    
 
Map 的使用避免声明冗余的类QueryMap 为Query 支持复杂的、不定数的字段。 对应的Body 也可以通过定义参数类型为Map 来避免声明冗余的类。 
以下代码为了Post/Put的Body特别定义了个IsReead类,实现方式有些重! 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class IsRead {       public boolean is_read;   }         public interface Api{       @Post // or @Put       Call<ResponseBody> reqIsRead(@Body IsRead isRead);   }               // send a post or put request   Api api = createApi();   IsRead isRead = new IsRead();   isRead.is_read = true;   api.reqIsRead(isRead); 
 
与如下代码的功能是相同,但更简单明了的:  
1 2 3 4 5 6 7 8 9 10 11 public interface Api{       @Post // or @Put       Call<ResponseBody> reqIsRead(@Body Map<String, Boolean> map);   }      // send a post or put request   Api api = createApi(); Map<String, Boolean> map = new HashMap<>(); map.put("is_read", true); api.reqIsRead(map); 
 
同样的,在请求的返回阶段,如果返回内容都是单纯的key: value ,那ResponseBody 也可以定义为Map 。 不必每个接口都有对应的数据类。  
 
RequestBody为String 及 文件上传 App中嵌套的 H5页面传给 App的内容是 Json格式化的字符串,并要作为 Body发起 Post /Put 请求,这时则希望RequestBody 为String ,则处理为:  
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Api {       @Post // or @Put       Call<ResponseBody> put(@Body RequestBody body);   }            // send a post or put request      Api api = create(Api.class);      String reqString = string;   RequestBody body = RequestBody.create(MediaType.parse("application/json"), reqString);   Call<ResponseBody> call = stringApi.post(body); 
 
如果将MediaType 改为图片、视频等对应的MediaType 值,则很可很方便的实现文件上传接口: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface Api {       @Multipart       @POST(ACT_POST_UPLOAD_PHOTO)       Call<UploadPhoto> reqUploadPhoto(@Part MultipartBody.Part file);   }            // upload photo file      File file = getPhotoFile();      // create RequestBody instance from file   MediaType mediaType = MediaType.parse("image/jpeg");   RequestBody requestFile = RequestBody.create(mediaType, file);   // MultipartBody.Part is used to send also the actual file name   MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);      // Optional:   // add another part within the multipart request   // String descriptionString = "hello, this is description speaking";   // RequestBody description = RequestBody.create(okhttp3.MultipartBody.FORM, descriptionString);      Call<UploadPhoto> call = accountApi.reqPostUploadPhoto(/*description, */body); 
 
 
后台Json空数据规范 客户端请求数据时,后台对空数据的返回应该是要有规范的, 应该按Json格式返回 []  空数组或 {}  空对象,不应什么都不返回.  
什么都不返回会导致Json解析异常,会误导客户端判定连接为CMCC/ChinaNet等假网络,导致提示网络异常,与实际情况不符。 
喂喂,后台同学,都是开发狗,能不能别互相伤害  
 
 
空数据Void声明 如果只需要发个请求然后根据response code 为HTTP_OK(200) 即可,而不需要后台回吐额外的数据,在定义接口时可以声明ResponseBody 为Void 。  
1 2 3 4 public interface Api {       @GET       Call<Void> reqIamOK();   } 
 
 
ResponseBody为String 当要将后台回吐的数据通过App 传参给内嵌的H5 页面时,这时希望ResponseBody 为String ,应该这么做: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 call.enqueue(new Callback<ResponseBody>() {       @Override       public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {           Log.d("Test", "post() respCode=" + response.code());           try {                              String str = response.body().string();               // do something……                    } catch (IOException e) {               e.printStackTrace();           }       }   }); 
 
 
ResponseBody的多次读取 当试图去读取response body 的原始数据时,由于是从网络上以stream 的方式读取的,所以多次读取的话会抛如下异常: 
1 2 3 4 5 6 java.lang.IllegalStateException: closed               at com.squareup.okhttp.internal.http.HttpConnection$FixedLengthSource.read(HttpConnection.java:455)               at okio.Buffer.writeAll(Buffer.java:594)               at okio.RealBufferedSource.readByteArray(RealBufferedSource.java:87)               at com.squareup.okhttp.ResponseBody.bytes(ResponseBody.java:56)               at com.squareup.okhttp.ResponseBody.string(ResponseBody.java:82) 
 
1 2 3 4 5 6 ResponseBody responseBody = response.body();   BufferedSource source = responseBody.source();   source.request(Long.MAX_VALUE); // Buffer the entire body.   Buffer buffer = source.buffer();   String responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"))   Log.d("TAG", responseBodyString); 
 
实现了多次读取的功能后,就可以进行下面的统一错误处理了。  
参考:HttpLoggingInterceptor 
 
统一的错误处理 发起一次请求后可能产生的错误有:  
网络问题:  
 
后台提示错误: 
请求参数不规范  
业务逻辑错误,如提交的内容包含敏感词   
Json解析失败 
response code不是HTTP_OK   
 
 
 
以上错误会分散在Callback的onResponse()及onFailure()中去。 不利于技术层的数据统计及业务层的错误兼容。 那么在做统一的错误处理时,目标有:  
onResponse()是纯粹的success回调,剥离了response code或解析失败等异常。     
能够读取后台返回的数据源进行处理后,不影响数据源的继续传播与解析。即上文提到的多次读取。    
 
能够统一进行错误处理的方式有Interceptor及对Callback进行override。 个人选择Callback override的方式,个人观点希望每个类是尽可能可复用的,对于每一次request,都有对应的Callback,那么就不想再定义新的类(Interceptor)来处理。 而且每一个request都有新的callback的实例对象,也好进行一些个性化的错误处理。 
新的Callback代码如下:  
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public abstract class MyCallback<T> implements Callback<T> {       protected boolean showToast; // 本次request的错误是否弹toast提醒       protected String logMark;       public MyCallback() {           showToast = true;       }       public MyCallback(boolean showToast) {           this.showToast = showToast;       }       /**        * Invoked for a received HTTP response.        * <p>        * Note: An HTTP response may still indicate an application-level failure such as a 404 or 500.        * Call {@link Response#isSuccessful()} to determine if the response indicates success.        */       @Override         public void onResponse(Call<T> call, Response<T> response){           int respCode = response.code();           Log.d("MyCallback", "onResponse() url[" + getLogMark(call) + "]" + respCode);           if (respCode == HttpURLConnection.HTTP_OK) {                 onResponse(call, response, respCode);           } else {               ResponseBody responseBody = null;               if (response != null) {                   responseBody = response.errorBody();               }               noHttpOK(respCode, responseBody);                  onFailure(call, null, response, respCode);           }       }          protected void noHttpOK(int respCode, ResponseBody respBody) {           int errorCode = 0;           String responseBodyString = "";              if (respBody != null) {               BufferedSource source = respBody.source();               try {                   source.request(Long.MAX_VALUE); // Buffer the entire body.               } catch (IOException e) {                   e.printStackTrace();               }               Buffer buffer = source.buffer();               try {                   responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"));                      Log.d("MyCallback", "noHttpOK() " + responseBodyString);               } catch (Throwable t) {                   t.printStackTrace();               }           }              ErrorEn responseEn = null;           try {               responseEn = GsonUtil.fromJson(responseBodyString, ErrorEn.class);           } catch (JsonSyntaxException jsExp) {               jsExp.printStackTrace();           }              if (respCode == HttpURLConnection.HTTP_FORBIDDEN) {               // permission error, do logout               // do Logout               Logout.do();           } else if (respCode == HttpURLConnection.HTTP_BAD_GATEWAY) {               if(showToast) {                   ToastUtil.showLongToast(BaseApplication.getInstance(),                           BaseApplication.getInstance().getString(R.string.error_network) + "(502)");               }           } else if (responseEn != null) {               String strMsg = responseEn.message;               if (respCode == HttpURLConnection.HTTP_PAYMENT_REQUIRED                       && responseEn.errorCode == Login.ELSE_WHERE_LOGIN) {                   //  异地登录,会弹对话框,所以不需要Toast重复提示                   showToast = false;               }               if (TextUtils.isEmpty(strMsg) == false) {                   if (showToast) {                         ToastUtil.showToast(BaseApplication.getInstance(), strMsg);                   }               }           }       }          /**        * Invoked when a network exception occurred talking to the server or when an unexpected        * exception occurred creating the request or processing the response.        */       @Override       public void onFailure(Call<T> call, Throwable t){           if (t != null) {               if(showToast) {                   ToastUtil.showLongToast(BaseApplication.getInstance(),                           BaseApplication.getInstance().getString(R.string.error_network));               }                  Log.d("MyCallback", "onFailure() url[" + getLogMark(call) + "]" + t.getMessage());           }           onFailure(call, t, null, -1);       }          /**        * @param respCode HttpURLConnection.responseCode.        * */       public abstract void onResponse(Call<T> call, Response<T> response, int respCode);          /**        * @param  response 当服务器返回的respCode不符合预期时,此处response为{@link Callback#onResponse(Call, Response)}中的response        * @param  respCode HttpURLConnection.responseCode.如果网络连续不成功/异常等,则值是-1.        * */       public abstract void onFailure(Call<T> call, @Nullable Throwable t, @Nullable Response<T> response, int respCode);      } 
 
 
About Sodino