资讯专栏INFORMATION COLUMN

基于retrofit的网络框架的终极封装(二)-与retrofit的对接与解耦,以及遇到的坑

isLishude / 2902人阅读

摘要:在上一篇基于的网络框架的终极封装一中介绍了顶层的设计这里再沿着代码走向往里说由于这里讲的是的封装性使用所以一些基础性的使用和配置这里就不讲了参数怎么传递到层的所有网络请求相关的参数和配置全部通过第一层的和链式调用封装到了中最后在方法中调用层

在上一篇基于retrofit的网络框架的终极封装(一)中介绍了顶层api的设计.这里再沿着代码走向往里说.
由于这里讲的是retrofit的封装性使用,所以一些retrofit基础性的使用和配置这里就不讲了.

参数怎么传递到retrofit层的?

所有网络请求相关的参数和配置全部通过第一层的api和链式调用封装到了ConfigInfo中,最后在start()方法中调用retrofit层,开始网络请求.

/**
 * 在这里组装请求,然后发出去
 * @param 
 * @return
 */
@Override
public  ConfigInfo start(ConfigInfo configInfo) {

    String url = Tool.appendUrl(configInfo.url, isAppendUrl());//组拼baseUrl和urltail
    configInfo.url = url;
    configInfo.listener.url = url;

    //todo 这里token还可能在请求头中,应加上此类情况的自定义.
    if (configInfo.isAppendToken){
        Tool.addToken(configInfo.params);
    }

    if (configInfo.loadingDialog != null && !configInfo.loadingDialog.isShowing()){
        try {//预防badtoken最简便和直接的方法
            configInfo.loadingDialog.show();
        }catch (Exception e){
        }
    }
    
    if (getCache(configInfo)){//异步,去拿缓存--只针对String类型的请求
        return configInfo;
    }
    T request = generateNewRequest(configInfo);//根据类型生成/执行不同的请求对象
    
    /*
    这三个方式是给volley预留的
    setInfoToRequest(configInfo,request);
    cacheControl(configInfo,request);
    addToQunue(request);*/

    return configInfo;
}

分类生成/执行各类请求:

 private  T generateNewRequest(ConfigInfo configInfo) {
    int requestType = configInfo.type;
    switch (requestType){
        case ConfigInfo.TYPE_STRING:
        case ConfigInfo.TYPE_JSON:
        case ConfigInfo.TYPE_JSON_FORMATTED:
            return  newCommonStringRequest(configInfo);
        case ConfigInfo.TYPE_DOWNLOAD:
            return newDownloadRequest(configInfo);
        case ConfigInfo.TYPE_UPLOAD_WITH_PROGRESS:
            return newUploadRequest(configInfo);
        default:return null;
    }
}

所以,对retrofit的使用,只要实现以下三个方法就行了:
如果切换到volley或者其他网络框架,也是实现这三个方法就好了.

newCommonStringRequest(configInfo),
newDownloadRequest(configInfo);
newUploadRequest(configInfo)
String类请求在retrofit中的封装:
 @Override
protected  Call newCommonStringRequest(final ConfigInfo configInfo) {
    Call call;
    if (configInfo.method == HttpMethod.GET){
        call = service.executGet(configInfo.url,configInfo.params);
    }else if (configInfo.method == HttpMethod.POST){
        if(configInfo.paramsAsJson){//参数在请求体以json的形式发出
            String jsonStr = MyJson.toJsonStr(configInfo.params);
            Log.e("dd","jsonstr request:"+jsonStr);
            RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);
            call = service.executeJsonPost(configInfo.url,body);
        }else {
            call = service.executePost(configInfo.url,configInfo.params);
        }
    }else {
        configInfo.listener.onError("不是get或post方法");//暂时不考虑其他方法
        call = null;
        return call;
    }
    configInfo.tagForCancle = call;

    call.enqueue(new Callback() {
        @Override
        public void onResponse(Call call, final Response response) {
            if (!response.isSuccessful()){
                        configInfo.listener.onCodeError("http错误码为:"+response.code(),response.message(),response.code());
                Tool.dismiss(configInfo.loadingDialog);
                return;
            }
            String string = "";
            try {
                string =  response.body().string();
                Tool.parseStringByType(string,configInfo);
                Tool.dismiss(configInfo.loadingDialog);

            } catch (final IOException e) {
                e.printStackTrace();
                        configInfo.listener.onError(e.toString());
                Tool.dismiss(configInfo.loadingDialog);
            }
        }
        @Override
        public void onFailure(Call call, final Throwable t) {

                    configInfo.listener.onError(t.toString());
            Tool.dismiss(configInfo.loadingDialog);
        }
    });
    return call;
}
service中通用方法的封装

既然要封装,肯定就不能用retrofit的常规用法:ApiService接口里每个接口文档上的接口都写一个方法,而是应该用QueryMap/FieldMap注解,接受一个以Map形式封装好的键值对.这个与我们上一层的封装思路和形式都是一样的.

@GET()
Call executGet(@Url String url, @QueryMap Map maps);

/**
 * 注意:
 * 1.如果方法的泛型指定的类不是ResonseBody,retrofit会将返回的string成用json转换器自动转换该类的一个对象,转换不成功就报错.
 *  如果不需要gson转换,那么就指定泛型为ResponseBody,
 *  只能是ResponseBody,子类都不行,同理,下载上传时,也必须指定泛型为ResponseBody
 * 2. map不能为null,否则该请求不会执行,但可以size为空.
 * 3.使用@url,而不是@Path注解,后者放到方法体上,会强制先urlencode,然后与baseurl拼接,请求无法成功.
 * @param url
 * @param map
 * @return
 */
@FormUrlEncoded
@POST()
Call executePost(@Url String url, @FieldMap Map map);
/**
 * 直接post体为一个json格式时,使用这个方法.注意:@Body 不能与@FormUrlEncoded共存
 * @param url
 * @param body
 * @return
 */
@POST()
Call executeJsonPost(@Url String url, @Body RequestBody body);
post参数体以json的形式发出时需要注意:

retrofit其实有请求时传入一个javabean的注解方式,确实可以在框架内部转换成json.但是不适合封装.

其实很简单,搞清楚以json形式发出参数的本质: 请求体中的json本质上还是一个字符串.那么可以将Map携带过来的参数转成json字符串,然后用RequestBody包装一层就好了:

 String jsonStr = MyJson.toJsonStr(configInfo.params);
 RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);
 call = service.executeJsonPost(configInfo.url,body);
不采用retrofit的json转换功能: Call的泛型不能采用二次泛型的形式--retrofit框架不接受:
@GET()
  Call> getStandradJson(@Url String url, @QueryMap Map maps);


//注:BaseNetBean就是三个标准字段的json:
public class BaseNetBean{
     public int code;
    public String msg;
    public T data;
}

这样写会抛出异常:
报的错误

Method return type must not include a type variable or wildcard: retrofit2.Call

JakeWharton的回复:
You cannot. Type information needs to be fully known at runtime in order for deserialization to work.

因为上面的原因,我们只能通过retrofit发请求,返回一个String,自己去解析.但这也有坑:

1.不能写成下面的形式:

@GET()
Call executGet(@Url String url, @QueryMap Map maps);

你以为指定泛型为String它就返回String,不,你还太年轻了.
这里的泛型,意思是,使用retrofit内部的json转换器,将response里的数据转换成一个实体类xxx,比如UserBean之类的,而String类明显不是一个有效的实体bean类,自然转换失败.
所以,要让retrofit不适用内置的json转换功能,你应该直接指定类型为ResponseBody:

@GET()
Call executGet(@Url String url, @QueryMap Map maps);

2.既然不采用retrofit内部的json转换功能,那就要在回调那里自己拿到字符串,用自己的json解析了.那么坑又来了:
泛型擦除:
回调接口上指定泛型,在回调方法里直接拿到泛型,这是在java里很常见的一个泛型接口设计:

public abstract class MyNetListener{
    public abstract void onSuccess(T response,String resonseStr);
    ....
}

//使用:
 call.enqueue(new Callback() {
        @Override
        public void onResponse(Call call, final Response response) {
          String  string =  response.body().string();
            Gson gson = new Gson();
            Type objectType = new TypeToken() {}.getType();
            final T bean = gson.fromJson(string,objectType);
            configInfo.listener.onSuccess(bean,string);
            ...
        }
        ...
    }

但是,抛出异常:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to xxx

这是因为在运行过程中,通过泛型传入的类型T丢失了,所以无法转换,这叫做泛型擦除:.
要解析的话,还是老老实实传入javabean的class吧.所以在最顶层的API里,有一个必须传的Class clazz:

postStandardJson(String url, Map map, Class clazz, MyNetListener listener)

综上,我们需要传入class对象,完全自己去解析json.解析已封装成方法.也是根据三个不同的小类型(字符串,一般json,标准json)

这里处理缓存时,如果要缓存内容,当然是缓存成功的内容,失败的就不必缓存了.

Tool.parseStringByType(string,configInfo);

 public static  void parseStringByType(final String string, final ConfigInfo configInfo) {
    switch (configInfo.type){
        case ConfigInfo.TYPE_STRING:
            //缓存
            cacheResponse(string, configInfo);
            //处理结果
             configInfo.listener.onSuccess(string, string);
            break;
        case ConfigInfo.TYPE_JSON:
             parseCommonJson(string,configInfo);
            break;
        case ConfigInfo.TYPE_JSON_FORMATTED:
            parseStandJsonStr(string, configInfo);
            break;
    }
}

json解析框架选择,gson,fastjson随意,不过最好也是自己再包一层api:

public static  T  parseObject(String str,Class clazz){
   // return new Gson().fromJson(str,clazz);
     return JSON.parseObject(str,clazz);
}

注意区分返回的是jsonObject还是jsonArray,有不同的解析方式和回调.
 private static  void parseCommonJson( String string, ConfigInfo configInfo) {
    if (isJsonEmpty(string)){
        configInfo.listener.onEmpty();
    }else {
        try{
            if (string.startsWith("{")){
                E bean =  MyJson.parseObject(string,configInfo.clazz);
                configInfo.listener.onSuccessObj(bean ,string,string,0,"");
                cacheResponse(string, configInfo);
            }else if (string.startsWith("[")){
                List beans =  MyJson.parseArray(string,configInfo.clazz);
                configInfo.listener.onSuccessArr(beans,string,string,0,"");
                cacheResponse(string, configInfo);
            }else {
                configInfo.listener.onError("不是标准json格式");
            }
        }catch (Exception e){
            e.printStackTrace();
            configInfo.listener.onError(e.toString());
        }
    }
}
标准json的解析:

三个字段对应的数据直接用jsonObject.optString来取:

        JSONObject object = null;
        try {
            object = new JSONObject(string);
        } catch (JSONException e) {
            e.printStackTrace();
            configInfo.listener.onError("json 格式异常");
            return;
        }
        String key_data = TextUtils.isEmpty(configInfo.key_data) ? NetDefaultConfig.KEY_DATA : configInfo.key_data;
        String key_code = TextUtils.isEmpty(configInfo.key_code) ? NetDefaultConfig.KEY_CODE : configInfo.key_code;
        String key_msg = TextUtils.isEmpty(configInfo.key_msg) ? NetDefaultConfig.KEY_MSG : configInfo.key_msg;

        final String dataStr = object.optString(key_data);
        final int code = object.optInt(key_code);
        final String msg = object.optString(key_msg);

注意,optString后字符串为空的判断:一个字段为null时,optString的结果是字符串"null"而不是null

public static boolean isJsonEmpty(String data){
        if (TextUtils.isEmpty(data) || "[]".equals(data)
                || "{}".equals(data) || "null".equals(data)) {
            return true;
        }
        return false;
    }

然后就是相关的code情况的处理和回调:
状态码为未登录时,执行自动登录的逻辑,自动登录成功后再重发请求.登录不成功才执行unlogin()回调.

注意data字段可能是一个普通的String,而不是json.

private static  void parseStandardJsonObj(final String response, final String data, final int code,
                                             final String msg, final ConfigInfo configInfo){

    int codeSuccess = configInfo.isCustomCodeSet ? configInfo.code_success : BaseNetBean.CODE_SUCCESS;
    int codeUnFound = configInfo.isCustomCodeSet ? configInfo.code_unFound : BaseNetBean.CODE_UN_FOUND;
    int codeUnlogin = configInfo.isCustomCodeSet ? configInfo.code_unlogin : BaseNetBean.CODE_UNLOGIN;

    if (code == codeSuccess){
        if (isJsonEmpty(data)){
            if(configInfo.isResponseJsonArray()){
                configInfo.listener.onEmpty();
            }else {
                configInfo.listener.onError("数据为空");
            }
        }else {
            try{
                if (data.startsWith("{")){
                    final E bean =  MyJson.parseObject(data,configInfo.clazz);
                     configInfo.listener.onSuccessObj(bean ,response,data,code,msg);
                    cacheResponse(response, configInfo);
                }else if (data.startsWith("[")){
                    final List beans =  MyJson.parseArray(data,configInfo.clazz);
                     configInfo.listener.onSuccessArr(beans,response,data,code,msg);
                    cacheResponse(response, configInfo);
                }else {//如果data的值是一个字符串,而不是标准json,那么直接返回
                    if (String.class.equals(configInfo.clazz) ){//此时,E也应该是String类型.如果有误,会抛出到下面catch里
                       configInfo.listener.onSuccess((E) data,data);
                    }else {
                        configInfo.listener.onError("不是标准的json数据");
                    }
                }
            }catch (final Exception e){
                e.printStackTrace();
                configInfo.listener.onError(e.toString());
                return;
            }
        }
    }else if (code == codeUnFound){
       configInfo.listener.onUnFound();
    }else if (code == codeUnlogin){
    //自动登录
        configInfo.client.autoLogin(new MyNetListener() {
            @Override
            public void onSuccess(Object response, String resonseStr) {
                configInfo.client.resend(configInfo);
            }
            @Override
            public void onError(String error) {
                super.onError(error);
                 configInfo.listener.onUnlogin();
            }
        });
    }else {
       configInfo.listener.onCodeError(msg,"",code);
    }
}
文件下载

先不考虑多线程下载和断点续传的问题,就单单文件下载而言,用retrofit写还是挺简单的

1.读写的超时时间的设置:

不能像上面字符流类型的请求一样设置多少s,而应该设为0,也就是不限时:

 OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)
            .connectTimeout(30, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //设置超时
           
2.接口需要声明为流式下载:
@Streaming //流式下载,不加这个注解的话,会整个文件字节数组全部加载进内存,可能导致oom
@GET
Call download(@Url String fileUrl);
3.声明了流式下载后,就能从回调而来的ResponseBody中拿到输入流(body.byteStream()),然后开子线程写到本地文件中去.

这里用的是一个异步任务框架,其实用Rxjava更好.

 SimpleTask simple = new SimpleTask() {
                @Override
                protected Boolean doInBackground() {
                    return writeResponseBodyToDisk(response.body(),configInfo.filePath);
                }
                @Override
                protected void onPostExecute(Boolean result) {
                    Tool.dismiss(configInfo.loadingDialog);
                    if (result){
                        configInfo.listener.onSuccess(configInfo.filePath,configInfo.filePath);
                    }else {
                        configInfo.listener.onError("文件下载失败");
                    }
                }
            };
            simple.execute();
            
            
进度回调的两种实现方式 最简单的,网络流写入到本地文件时,获得进度(writeResponseBodyToDisk方法里)
byte[] fileReader = new byte[4096];
long fileSize = body.contentLength();
long fileSizeDownloaded = 0;
inputStream = body.byteStream();
 outputStream = new FileOutputStream(futureStudioIconFile);
 while (true) {
     int read = inputStream.read(fileReader);
     if (read == -1) {
       break;
      }
    outputStream.write(fileReader, 0, read);
    fileSizeDownloaded += read;
    Log.d("io", "file download: " + fileSizeDownloaded + " of " + fileSize);//  这里也可以实现进度监听
 }
利用okhttp的拦截器

1.添加下载时更新进度的拦截器

okHttpClient .addInterceptor(new ProgressInterceptor())

2.ProgressInterceptor:实现Interceptor接口的intercept方法,拦截网络响应

    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException{
        Response originalResponse = chain.proceed(chain.request());
        return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),chain.request().url().toString())).build();
    }

3 ProgressResponseBody: 继承 ResponseBody ,在内部网络流传输过程中读取进度:

public class ProgressResponseBody extends ResponseBody {
    private final ResponseBody responseBody;
    private BufferedSource bufferedSource;
    private String url;

    public ProgressResponseBody(ResponseBody responseBody,String url) {
        this.responseBody = responseBody;
        this.url = url;

    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }


    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }
    long timePre = 0;
    long timeNow;

    private Source source(final Source source) {
        return new ForwardingSource(source) {
            long totalBytesRead = 0L;
            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                timeNow = System.currentTimeMillis();
                if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次状态
                    timePre = timeNow;
                    EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(),
                            totalBytesRead == responseBody.contentLength(),url));
                }
                return bytesRead;
            }
        };
    }
}
进度数据以event的形式传出(采用Eventbus),在listener中接收

一般进度数据用于更新UI,所以最好设置数据传出的时间间隔,不要太频繁:

事件的发出:
timeNow = System.currentTimeMillis();
if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次状态
    timePre = timeNow;
     EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(), totalBytesRead == responseBody.contentLength(),url));
    }
事件的接收(MyNetListener对象中):

注意: MyNetListener与url绑定,以防止不同下载间的进度错乱.

@Subscribe(threadMode = ThreadMode.MAIN)
public void  onMessage(ProgressEvent event){
    if (event.url.equals(url)){
        onProgressChange(event.totalLength,event.totalBytesRead);
        if (event.done){
            unRegistEventBus();
            onFinish();
        }
    }
}
文件上传

文件上传相对于普通post请求有区别,你非常需要了解http文件上传的协议:

1.提交一个表单,如果包含文件上传,那么必须指定类型为multipart/form-data.这个在retrofit中通过@Multipart注解指定即可.

2.表单中还有其他键值对也要一同传递,在retrofit中通过@QueryMap以map形式传入,这个与普通post请求一样

3.服务器接收文件的字段名,以及上传的文件路径,通过@PartMap以map形式传入.这里的字段名对应请求体中Content-Disposition中的name字段的值.大多数服务器默认是file.(因为SSH框架默认的是file?)

4.请求体的content-type用于标识文件的具体MIME类型.在retrofit中,是在构建请求体RequestBody时指定的.需要我们指定.
那么如何获得一个文件的MIMe类型呢?读文件的后缀名的话,不靠谱.最佳方式是读文件头,从文件头中拿到MIME类型.不用担心,Android有相关的api的

综上,相关的封装如下:

同下载一样,配置httpclient时,读和写的超时时间都要置为0
OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)
                                .connectTimeout(0, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //设置超时
ApiService中通用接口的定义
@POST()
@Multipart
Call uploadWithProgress(@Url String url,@QueryMap Map options,@PartMap Map fileParameters) ;
key-filepath到key-RequestBody的转换:

这里的回调就不用开后台线程了,因为流是在请求体中,而retrofit已经帮我们搞定了请求过程的后台执行.

protected  Call newUploadRequest(final ConfigInfo configInfo) {
    if (serviceUpload == null){
        initUpload();
    }
    configInfo.listener.registEventBus();
    Map requestBodyMap = new HashMap<>();
    if (configInfo.files != null && configInfo.files.size() >0){
        Map files = configInfo.files;
        int count = files.size();
        if (count>0){
            Set> set = files.entrySet();
            for (Map.Entry entry : set){
                String key = entry.getKey();
                String value = entry.getValue();
                File file = new File(value);
                String type = Tool.getMimeType(file);//拿到文件的实际类型
                Log.e("type","mimetype:"+type);
                UploadFileRequestBody fileRequestBody = new UploadFileRequestBody(file, type,configInfo.url);
                requestBodyMap.put(key+""; filename="" + file.getName(), fileRequestBody);
            }
        }
    }

    Call call = service.uploadWithProgress(configInfo.url,configInfo.params,requestBodyMap);
    
注意,RequestBody中的content-type不是multipart/form-data,而是文件的实际类型.multipart/form-data是请求头中的文件上传的统一type.
    public class UploadFileRequestBody extends RequestBody {
        private RequestBody mRequestBody;
        private BufferedSink bufferedSink;
        private String url;
        public UploadFileRequestBody(File file,String mimeType,String url) {
           // this.mRequestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
            this.mRequestBody = RequestBody.create(MediaType.parse(mimeType), file);
            this.url = url;
        }
         @Override
        public MediaType contentType() {
            return mRequestBody.contentType();
        }
进度的回调

封装在UploadFileRequestBody中,无需通过okhttp的拦截器实现,因为可以在构建RequestBody的时候就包装好(看上面代码),就没必要用拦截器了.

最后的话

到这里,主要的请求执行和回调就算讲完了,但还有一些,比如缓存控制,登录状态的维护,以及cookie管理,请求的取消,gzip压缩,本地时间校准等等必需的辅助功能的实现和维护,这些将在下一篇文章进行解析.

代码

https://github.com/hss01248/NetWrapper

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/13364.html

相关文章

  • Android框架 - 收藏集 - 掘金

    摘要:说好的分类集合来啦,采用标签云的方式来展示掘金阅读提示点击下文中返回到顶部有分类不合理的地方请提。反编译这个后发现其使用个优质的开源项目掘金是由的开源的一个库,用于构建可预期的和声明式的用户界面。 想不想通过一线互联网公司面试? - Android - 掘金国内一线互联网公司内部面试题库 以下面试题来自于百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐内部题库 熟悉本文中列出的知...

    zengdongbao 评论0 收藏0
  • Android框架 - 收藏集 - 掘金

    摘要:说好的分类集合来啦,采用标签云的方式来展示掘金阅读提示点击下文中返回到顶部有分类不合理的地方请提。反编译这个后发现其使用个优质的开源项目掘金是由的开源的一个库,用于构建可预期的和声明式的用户界面。 想不想通过一线互联网公司面试? - Android - 掘金国内一线互联网公司内部面试题库 以下面试题来自于百度、小米、乐视、美团、58、猎豹、360、新浪、搜狐内部题库 熟悉本文中列出的知...

    awesome23 评论0 收藏0
  • 基于retrofit网络框架终极封装(一):第一层(参数组装层)API设计

    摘要:什么是分层的架构不管是架构演变中贯穿始终的概念都是分层和解耦那么这个分层和解耦怎么体现出来简单地说就是我这一层接收上一层的输入上一层的你别管我怎么处理我最终会给你一个输出返回值你完全不用理会我是怎么处理的只要有输入就会有输出而且一般是通过一 什么是分层 app的架构,不管是MVC,MVP,MVVM,架构演变中,贯穿始终的概念都是分层和解耦.那么这个分层和解耦怎么体现出来?简单地说就是,...

    includecmath 评论0 收藏0
  • Android网络框架

    摘要:无网读缓存,有网根据过期时间重新请求使用和实现网络缓存。浅析的你了解过吗为什么每次请求都用了长连接完成一次网络请求都经历了什么感兴趣的不妨可以看下。 Android OKHttp3.0 以上使用方法 Android OKHttp3.0 以上使用方法详解 Retrofit 之日志拦截 Retrofit 日志拦截相关介绍 Retrofit源码解析 Retrofit的源码分析将从基本的使用方...

    smallStone 评论0 收藏0
  • retrofit - 收藏集 - 掘金

    摘要:一本适合基础入门的中文翻译书掘金中文翻译版本书是对所写一书的中文翻译版本,仅供交流学习使用,严禁商业用途。我们在中使用监测事件掘金源码解析掘金看看调用的代码又是什么抱着一贯的好奇,点进去看看。 一本适合 RxJava 基础入门的中文翻译书 - Android - 掘金RxJava Essentials 中文翻译版 本书是对Ivan.Morgillo所写一书的中文翻译版本,仅供交流学习使...

    Fourierr 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<