基于Retrofit2封装网络请求框架

写在前面

之前基于OkHttp3封装了一个网络请求框架,本篇接着上一篇的内容继续对Retrofit作一个封装,所以首先基本的Retrofit的用法你要清楚,起码了解过它发起请求的一个完整过程。最近这些天我对这个网络请求库进行了简单的测试,因为处理的业务并不多,所以暂时还没有出现什么太大的问题,这篇文章在草稿箱里封存了这么久了,今天是2月12号了,年前事年前毕,明天就回家了,今天把它放出来了,如果大家发现了什么问题,欢迎给我留言。

一、项目地址

GitHub地址:https://github.com/square/retrofit

官网地址:http://square.github.io/retrofit/

个人封装Retrofit案例地址:https://github.com/JArchie/NetTest

二、封装思路讲解(这里是摘取核心代码说明封装思路,完整代码参见上面给出的案例地址)

(一)基本请求的实现

1、创建项目

首先我们新建一个工程,然后在该工程下New Module,选择Android Library,然后新建一个包名称为retrofit,我们所有封装的网络请求相关的类都放在这个包下面,主要用来进行RESTful请求的(一种软件的架构,推荐阮一峰大神的这篇文章:http://www.ruanyifeng.com/blog/2011/09/restful.html)。在具体代码实现之前,我们还必须要做的一步操作是,把retrofit相关的依赖都添加到build.gradle文件中,大家可以去项目主页上按步骤添加,这里我就直接从我的项目中复制了:

//网络库
compile 'com.squareup.okhttp3:okhttp:3.9.1'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:converter-scalars:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'

为了使封装更加灵活,基于传入参数,并且无顺序要求的原则,建造者模式无疑是最佳的选择。

2、创建Service接口

首先Retrofit的使用必须要有一个接口,这里定义为RestService,里面定义一系列的方法,用来发起具体的请求,为了更好的理解我下面代码的含义,这里带大家先来了解一下几个注解的具体含义:

Http请求方法注解:

基于Retrofit2封装网络请求框架_第1张图片

标记类注解:

基于Retrofit2封装网络请求框架_第2张图片

参数类注解:

基于Retrofit2封装网络请求框架_第3张图片

鉴于是通用的封装,所以这里不会传入具体的路由信息,这里参数均使用Map以键值对的形式定义,因为value不确定具体的类型,所以我们类型给它Object,然后每个方法的返回类型都是Retrofit2中的Call类型,如果你要使用RxJava,这里需要返回Observable类型,之后我们需要操作这个Call对象,来看本类的代码:

public interface RestService {

    //Get请求
    @GET
    Call get(@Url String url, @QueryMap Map params);

    //Post请求
    @FormUrlEncoded
    @POST
    Call post(@Url String url, @FieldMap Map params);

    //Post原始数据
    @POST
    Call postRaw(@Url String url, @Body RequestBody body);

    //Put请求
    @FormUrlEncoded
    @PUT
    Call put(@Url String url, @FieldMap Map params);

    //Put原始数据
    @PUT
    Call putRaw(@Url String url, @Body RequestBody body);

    //Delete请求
    @DELETE
    Call delete(@Url String url, @QueryMap Map params);

    //文件下载
    @Streaming
    @GET
    Call download(@Url String url, @QueryMap Map params);

    //文件上传
    @Multipart
    @POST
    Call upload(@Url String url, @Part MultipartBody.Part file);

}

3、创建请求方法类型管理类

新建一个类HttpMethod,这个类用来统一管理请求方法的类型,这个类其实是可有可无的,但是本着类多代码少的原则,类的数量多,类中代码少,这样整体的架构将会更加清晰,代码看着也更加整洁,这也是面向对象六大原则中单一职责原则(SRP)所推荐的(注意:这个尽量不要使用枚举类型,因为使用枚举内存会飙升,本人一开始就是定义的枚举类,后来Review代码时又改成常量类了),具体代码体现如下:

/**
 * Created by Jarchie on 2017\12\7.
 * 统一管理请求方法类型
 */

public final class HttpMethod {
    public static final String GET = "GET";
    public static final String POST = "POST";
    public static final String POST_RAW = "POST_RAW";
    public static final String PUT = "PUT";
    public static final String PUT_RAW = "PUT_RAW";
    public static final String DELETE = "DELETE";
    public static final String UPLOAD = "UPLOAD";
}

4、创建Retrofit的构建类

这里新建一个类RestCreator,这里面会使用到Java并发编程中推荐的单例模式的创建方式——内部类Holder,这里创建了一个静态内部类RetrofitHolder,用来构建全局的Retrofit对象,这里配置了Retrofit的baseUrl、client、并且添加了它的转换器,在配置client属性时,因为它需要一个OkHttpClient,所以我们这里又创建了一个静态内部类OkHttpHolder用来构建OkHttpClient对象,然后传入Retrofit的client属性中,通过写代码我们可以发现,Retrofit和OkHttp本身在设计的时候也是大量的使用了建造者模式,可见这种设计模式还是很受欢迎的,但是有一点比较烦,就是写起来比较累,很繁琐,后面在代码中会有详细的体现。又扯远了啊,继续说这个类,我们还在这个类中创建了OKHTTP的日志拦截器,这样我们在请求有响应时可以通过日志信息清楚的看到返回的数据,具体代码如下所示:

public final class RestCreator {

    private static final class RetrofitHolder {
        private static final String BASE_URL = Constant.BASE_URL;
        private static final Retrofit RETROFIT_CLIENT = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(OKHttpHolder.OK_HTTP_CLIENT)
                .addConverterFactory(ScalarsConverterFactory.create())
                .build();
    }

    private static final class OKHttpHolder {
        private static final int TIME_OUT = 20;
        private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
                .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
                .addInterceptor(getLoggerInterceptor())
                .build();
    }

    //创建OKHTTP的日志拦截器
    private static HttpLoggingInterceptor getLoggerInterceptor() {
        //日志显示级别
        HttpLoggingInterceptor.Level level = HttpLoggingInterceptor.Level.BODY;
        //新建log拦截器
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(
                new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(@NonNull String message) {
                Log.e("ResponseBody------->", message);
            }
        });
        loggingInterceptor.setLevel(level);
        return loggingInterceptor;
    }

}

5、请求回调的处理

这里我们新建一个包callback,在这个包下我们新建四个接口IRequest、ISuccess、IFailure、IError,相对应的分别用来处理请求开始结束、请求成功、请求失败、请求错误等几种状态下的回调。具体代码如下:

public interface IRequest {
    //请求开始
    void onRequestStart();

    //请求结束
    void onRequestEnd();
}
public interface ISuccess {
    //请求成功
    void onSuccess(Object response);
}
public interface IFailure {
    //请求失败
    void onFailure();
}
public interface IError {
    //请求错误
    void onError(int code, String msg);
}

接着我们创建一个RequestCallbacks类,该类实现Retrofit2包下的Callback接口,这里注意别导错包了,这个类我们用来具体处理网络请求的回调过程,这个类中我创建了一个class字节码变量,在返回成功时使用Gson解析,将解析返回的结果就是一个数据实体对象返回到应用层中,在ISuccess接口中我们的参数类型定义为了Object,所以可以很灵活的强转成你自己的实体类型。如果你不想在框架层就解析成实体,比如有些业务情况返回的JSON数据比较简单,这里也做了一层处理,如果有实体对象就解析,没有就直接将原生JSON返回到应用层中去,做到了灵活处理。

public class RequestCallbacks implements Callback {

    private final IRequest REQUEST;
    private final ISuccess SUCCESS;
    private final IFailure FAILURE;
    private final IError ERROR;
    private final Class CLASS;

    public RequestCallbacks(IRequest request, ISuccess success, IFailure failure, IError error, Class clazz) {
        this.REQUEST = request;
        this.SUCCESS = success;
        this.FAILURE = failure;
        this.ERROR = error;
        this.CLASS = clazz;
    }

    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response) {
        if (response.isSuccessful()) {
            if (call.isExecuted()) {
                if (SUCCESS != null) {
                    if (CLASS != null) {
                        Object object = new Gson().fromJson(response.body(), CLASS);
                        SUCCESS.onSuccess(object);
                    } else {
                        SUCCESS.onSuccess(response.body());
                    }
                }
            }
        } else {
            if (ERROR != null) {
                ERROR.onError(response.code(), response.message());
            }
        }
        DialogLoader.dismiss();
    }

    @Override
    public void onFailure(@NonNull Call call, @NonNull Throwable t) {
        if (FAILURE != null) {
            FAILURE.onFailure();
        }
        if (REQUEST != null) {
            REQUEST.onRequestEnd();
        }
        DialogLoader.dismiss();
    }

}

6、开启建造者模式

准备工作结束之后,我们来开始建造者的创建。这里首先创建一个宿主类RestClient,然后再创建一个建造者类RestClientBuilder,好了文件创建完成了先放着不要急着写,我们先来思考一下网络请求一般会有哪些参数?我们很容易想到的参数如下:URL、传入的值(请求参数)、回调、请求体(RequestBody这是OkHttp3中的内容)这些东西,别忘了定义我们上面准备好的字节码变量,用来控制数据解析的哦。我们的RestClient在每次Builder去build的时候,它都会生成一个全新的实例,这里面的参数是一次构建完毕,绝不允许更改的,所以我们在声明的时候使用final关键字来声明,这样能保证每一次传值的原子性,在多线程中也是一种比较安全的做法。用final声明的变量如果没有赋值的话,必须在构造方法中为其赋值,来看代码:

private final Context CONTEXT;
private final String URL;
private final HashMap PARAMS;
private final RequestBody BODY;
private final IRequest REQUEST;
private final ISuccess SUCCESS;
private final IFailure FAILURE;
private final IError ERROR;
private final Class CLASS;

public RestClient(Context context, String url, HashMap params,
                  RequestBody body, IRequest request,
                  ISuccess success, IFailure failure, IError error,
                  Class clazz) {
    this.CONTEXT = context;
    this.URL = url;
    this.PARAMS = params;
    this.BODY = body;
    this.REQUEST = request;
    this.SUCCESS = success;
    this.FAILURE = failure;
    this.ERROR = error;
    this.CLASS = clazz;
}

这样我们就可以创建我们的建造者了,代码如下:

public static RestClientBuilder Builder() {
    return new RestClientBuilder();
}

我们接着来看一下RestClientBuilder这个类里面有哪些方法?Builder里面就是一些传值的操作,所以我们需要把宿主类里面的参数都照搬过来,当然了这里不能再使用final关键字修饰了,否则我们不能为其依次赋值了,所以我们就按照普通的方式来声明了:

private Context mContext = null;
private String mUrl = null;
private HashMap PARAMS = new HashMap<>();
private RequestBody mBody = null;
private IRequest mIRequest = null;
private ISuccess mISuccess = null;
private IFailure mIFailure = null;
private IError mIError = null;
private Class mClass = null;

这里我们不允许外部的类去直接new它,只允许同包的RestClient去new它,所以这里在构造方法中做一个限制:

RestClientBuilder() {}

接下来是一系列的具体构建上面声明的这些参数的方法,这里我们同样使用了final关键字修饰这些方法,不允许外部修改它,构建这些参数的代码其实都是很类似的,写了一个其它的直接复制粘贴,修修改改就OK了,来看具体代码:

public final RestClientBuilder context(Context context) {
    this.mContext = context;
    return this;
}

public final RestClientBuilder url(String url) {
    this.mUrl = url;
    return this;
}

public final RestClientBuilder params(HashMap params) {
    PARAMS.putAll(params);
    return this;
}

public final RestClientBuilder params(String key, Object value) {
    PARAMS.put(key, value);
    return this;
}

public final RestClientBuilder raw(String raw) {
    this.mBody = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), raw);
    return this;
}

public final RestClientBuilder onRequest(IRequest iRequest) {
    this.mIRequest = iRequest;
    return this;
}

public final RestClientBuilder listener(ISuccess iSuccess) {
    this.mISuccess = iSuccess;
    return this;
}

public final RestClientBuilder listener(IFailure iFailure) {
    this.mIFailure = iFailure;
    return this;
}

public final RestClientBuilder listener(IError iError) {
    this.mIError = iError;
    return this;
}

public final RestClientBuilder clazz(Class clazz) {
    this.mClass = clazz;
    return this;
}

好了,接着我们来build我们的RestClient,其实就是宿主的RestClient通过它自己的建造者RestClientBuilder去返回它本身的对象:

public final RestClient build() {
    return new RestClient(mContext, mUrl, PARAMS, mBody, mIRequest,
            mISuccess, mIFailure, mIError, mClass);
}

7、发起网络请求

构建了RestClient对象之后,我们还没有调起网络请求的方法呢,不然写了这么多也是毫无卵用。

首先我们要拿到RestService接口的对象,这里为了方便,在RestCreator这个类中使用内部类Holder的模式去实现,然后对外提供一个返回RestService类对象的静态方法去获取:

public static RestService getRestService() {
    return RestServiceHolder.REST_SERVICE;
}

private static final class RestServiceHolder {
    private static final RestService REST_SERVICE =
            RetrofitHolder.RETROFIT_CLIENT.create(RestService.class);
}

拿到RestService之后,我们来接着写我们的实现请求的方法request(String method)方法,参数是区分各个请求类型的字符串,之前我们已经在HttpMethod类中定义好了,不知道大家还有印象吗?在这个方法中我们通过RestService类对象去调用内部定义好的通用的请求方法(get、post、put、Delete等等),然后返回一个Call对象,注意也是Retrofit2包下面的。调用完了具体的方法之后,我们最后去处理retrofit的回调,就是我们上面定义好的RequestCallbacks类中的内容,代码如下:

private void request(String method) {
    final RestService service = RestCreator.getRestService();
    Call call = null;

    if (REQUEST != null) {
        REQUEST.onRequestStart();
    }

    //弹出加载框
    DialogLoader.show(CONTEXT);

    switch (method) { //调起Service中相对应的请求类型
        case HttpMethod.GET: //GET请求
            call = service.get(URL, PARAMS);
            break;
        case HttpMethod.POST: //POST请求
            call = service.post(URL, PARAMS);
            break;
        case HttpMethod.POST_RAW: //POST原始数据
            call = service.postRaw(URL, BODY);
            break;
        case HttpMethod.PUT: //PUT请求
            call = service.put(URL, PARAMS);
            break;
        case HttpMethod.PUT_RAW: //PUT原始数据
            call = service.putRaw(URL, BODY);
            break;
        case HttpMethod.DELETE: //DELETE请求
            call = service.delete(URL, PARAMS);
            break;
        case HttpMethod.UPLOAD: //上传文件
            final RequestBody requestBody =
                    RequestBody.create(MediaType.parse(MultipartBody.FORM.toString()), FILE);
            final MultipartBody.Part body =
                    MultipartBody.Part.createFormData("file", FILE.getName(), requestBody);
            call = service.upload(URL, body);
            break;
        default:
            break;
    }
    if (call != null) {
        call.enqueue(getRequestCallback());
    }
}

//获取处理回调的方法
private Callback getRequestCallback() {
    return new RequestCallbacks(REQUEST, SUCCESS, FAILURE, ERROR, CLASS);
}

最后我们再定义如下几个方法:get()、post()、put()、delete()、upload(),方法内部去调用request()方法,在建造者构建完成时调用,用来真正发起相对应的请求:

//GET请求
public final void get() {
    request(HttpMethod.GET);
}

//POST请求
public final void post() {
    if (BODY == null) {
        request(HttpMethod.POST);
    } else {
        if (!PARAMS.isEmpty()) {
            throw new RuntimeException("params must be null!");
        }
        request(HttpMethod.POST_RAW);
    }
}

//PUT请求
public final void put() {
    if (BODY == null) {
        request(HttpMethod.PUT);
    } else {
        if (!PARAMS.isEmpty()) {
            throw new RuntimeException("params must be null!");
        }
        request(HttpMethod.PUT_RAW);
    }
}

//DELETE请求
public final void delete() {
    request(HttpMethod.DELETE);
}

//上传文件
public final void upload() {
    request(HttpMethod.UPLOAD);
}

8、调用说明

在做完了以上这些工作之后,其实我们就已经把我们的RestClient的雏形构建出来了,链式调用结构清晰,让人看着神清气爽啊,发起请求的代码就写成了这个样子了:

 

RestClient.Builder()
        .context(this)
        .clazz(null)
        .params("","")
        .listener(new ISuccess() {
            @Override
            public void onSuccess(Object response) {
                
            }
        })
        .listener(new IFailure() {
            @Override
            public void onFailure() {
                
            }
        })
        .listener(new IError() {
            @Override
            public void onError(int code, String msg) {
                
            }
        })
        .build()
        .post();

 

三、文件下载

文件下载其实就是个GET请求,这里需要注意大文件下载时一定要添加@Streaming注解,这是官方提到的一点,因为文件下载时是一次性读到内存中的,不加这个很容易造成内存溢出。下面我们就来具体的实现一下文件下载,其实过程都差不多。

1、处理耗时任务

因为文件下载是一个耗时的过程,所以我们不能直接在主线程中进行,需要在子线程中处理,然后将结果转发到主线程中。这里我们新建一个类SaveFileTask继承自AsyncTask类,第一个输入参数类型我们传入Object,第二个处理过程参数类型传入Void,第三个输出参数类型我们传入File类型,在doInBackground方法中,首先是拿到文件目录、后缀名、文件名、输入流这些内容,然后通过IO流进行文件写入的操作,在onPostExecute方法中,处理执行完的结果,此时是被主线程调用的,简单起见只处理成功的回调,通过SUCCESS.onSuccess()方法,参数传入文件路径,将下载的结果进行返回,如下所示:

public final class SaveFileTask extends AsyncTask {

    private final IRequest REQUEST;
    private final ISuccess SUCCESS;

    public SaveFileTask(IRequest request, ISuccess success) {
        this.REQUEST = request;
        this.SUCCESS = success;
    }

    @Override
    protected File doInBackground(Object... params) {
        String downloadDir = (String) params[0];
        String extension = (String) params[1];
        final ResponseBody body = (ResponseBody) params[2];
        final String name = (String) params[3];
        final InputStream is = body.byteStream(); //获得输入流
        if (downloadDir == null || downloadDir.equals("")) {
            downloadDir = "down_loads"; //下载的文件目录
        }
        if (extension == null || extension.equals("")) {
            extension = "";
        }
        if (name == null) {
            return FileUtil.writeToDisk(is, downloadDir, extension.toUpperCase(), extension);
        } else {
            return FileUtil.writeToDisk(is, downloadDir, name);
        }
    }

    @Override
    protected void onPostExecute(File file) {
        super.onPostExecute(file);
        if (SUCCESS != null) {
            SUCCESS.onSuccess(file.getPath());
        }
        if (REQUEST != null) {
            REQUEST.onRequestEnd();
        }
        autoInstallApk(file);
    }

    //下载apk文件下载完成时自动安装程序
    private void autoInstallApk(File file) {
        if (FileUtil.getExtension(file.getPath()).equals("apk")) {
            final Intent install = new Intent();
            install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            install.setAction(Intent.ACTION_VIEW);
            install.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            Contract.getContext().startActivity(install);
        }
    }

}

2、处理下载逻辑及回调

新建一个类DownloadHandler,然后定义好网络请求的URL、参数、文件目录、后缀名、文件名、相关回调接口,最后通过RestService调用内部定义的download方法,实现Callback,泛型传入ResponseBody,在重写的成功和失败的回调方法里进行具体的实现,成功的回调中通过我们上面定义的SaveFileTask类的对象去调用下载的执行方法进行真正的下载,这里调用的是executeOnExecutor方法,它通常和THREAD_POOL_EXECUTOR一起使用,允许多个任务在由AsyncTask管理的线程池中并行执行,代码如下:

public class DownloadHandler {
    private final String URL;
    private final HashMap PARAMS;
    private final IRequest REQUEST;
    private final String DOWNLOAD_DIR;
    private final String EXTENSION;
    private final String NAME;
    private final ISuccess SUCCESS;
    private final IFailure FAILURE;
    private final IError ERROR;

    public DownloadHandler(String url, HashMap params,
                           IRequest request, String downloadDir,
                           String extension, String name,
                           ISuccess success, IFailure failure,
                           IError error) {
        this.URL = url;
        this.PARAMS = params;
        this.REQUEST = request;
        this.DOWNLOAD_DIR = downloadDir;
        this.EXTENSION = extension;
        this.NAME = name;
        this.SUCCESS = success;
        this.FAILURE = failure;
        this.ERROR = error;
    }

    //处理文件下载
    public final void handleDownload() {
        if (REQUEST != null) {
            REQUEST.onRequestStart();
        }
        RestCreator.getRestService().download(URL, PARAMS)
                .enqueue(new Callback() {
                    @Override
                    public void onResponse(@NonNull Call call, @NonNull Response response) {
                        if (response.isSuccessful()) { //成功时的回调
                            final ResponseBody responseBody = response.body();
                            final SaveFileTask task = new SaveFileTask(REQUEST, SUCCESS);
                            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
                                    DOWNLOAD_DIR, EXTENSION, responseBody, NAME);
                            //这里一定要注意判断,否则文件下载不全
                            if (task.isCancelled()) {
                                if (REQUEST != null) {
                                    REQUEST.onRequestEnd();
                                }
                            }
                        } else { //错误时的回调
                            ERROR.onError(response.code(), response.message());
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call call, @NonNull Throwable t) {
                        if (FAILURE != null)
                            FAILURE.onFailure();
                    }
                });
    }

}

3、添加到构建者模式

我们通过和之前一样的方法,在RestClient类中新增下载文件的一些参数,并且在构造方法中补上:

private final File FILE; //文件
private final String DOWNLOAD_DIR; //文件目录
private final String EXTENSION; //后缀名
private final String NAME; //文件名

还要提供一个下载的调用方法,内部实现直接构造一个DownloadHandler对象即可:

//下载文件
public final void download() {
    new DownloadHandler(URL, PARAMS, REQUEST, DOWNLOAD_DIR,
            EXTENSION, NAME, SUCCESS, FAILURE, ERROR)
            .handleDownload();
}

并且在RestClientBuilder类中提供相对应的构建方法:

public final RestClientBuilder file(File file) {
    this.mFile = file;
    return this;
}

public final RestClientBuilder file(String filePath) {
    this.mFile = new File(filePath);
    return this;
}

public final RestClientBuilder dir(String dir) {
    this.mDownloadDir = dir;
    return this;
}

public final RestClientBuilder extension(String extension) {
    this.mExtension = extension;
    return this;
}

public final RestClientBuilder name(String name) {
    this.mName = name;
    return this;
}

好了到这里,我们的整个框架就已经封装完成了,不过还是有很多需要完善的地方,我只能日后再说了!

四、编写测试案例

下面在应用层写一个测试案例,来测试框架是否能够正常使用,这里我就只贴实现的核心代码了(文章太长估计都没耐心看了):

接口地址(猫眼电影(非官方)):http://m.maoyan.com/movie/list.json?type=hot&offset=0&limit=10

请求代码如下:

RestClient.Builder()
        .context(this)
        .url("movie/list.json?type=hot")
        .params("offset", 0)
        .params("limit", 10)
        .clazz(MovieBean.class)
        .listener(new ISuccess() {
            @Override
            public void onSuccess(Object response) {
                MovieBean bean = (MovieBean) response;
                mList.addAll(bean.getData().getMovies());
                recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
                recyclerView.setAdapter(new IndexListAdapter(MainActivity.this, mList));
            }
        })
        .listener(new IError() {
            @Override
            public void onError(int code, String msg) {
                Log.e("onError: ", msg);
            }
        })
        .listener(new IFailure() {
            @Override
            public void onFailure() {
                Log.e("onFailure: ", "请求失败");
            }
        })
        .build()
        .get();

最终实现的效果图如下图所示:

基于Retrofit2封装网络请求框架_第4张图片

好了,写到这里就要结束了,最后再甩一遍本项目的地址:https://github.com/JArchie/NetTest

最后祝大家新年快乐,阖家幸福!

你可能感兴趣的:(Android基础业务)