使用Retrofit2封装适用于组件化项目的网络库

2,677 阅读15分钟
原文链接: m.blog.csdn.net

版权声明:本文为博主原创文章,未经我的允许不得转载!
转载请标明出处: blog.csdn.net/guiying712/… ,本文出自:【张华洋的博客】


1、URL的简单构成

URL(Uniform Resource Locator)定义:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URL构成一般是这样的:

[ scheme : ][ //authority ][ path ][ ? query ]

名称 功能
schema 访问服务器以获取资源时要使用哪种协议,比如,http,https和FTP等
host HTTP服务器的IP地址或域名
port HTTP服务器的默认端口是80,这种情况下端口号可以省略,如果使用了别的端口,必须指明,例如www.cnblogs.com:8080
authority 将host和Port用冒号连接组成authority ,authority = host:port
path 访问资源的路径
query 发给 http 服务器的数据

URL的例子:

www.cnblogs.com:8080/yourpath/md…

  • scheme : http
  • authority : www.cnblogs.com:8080
  • path : /yourpath/mdeditor
  • query : 在 后的部分为 stove=10&path=1&id=6

因为 authority又进一步可以划分为 host:port形式,其中 host:port 用冒号分割,冒号前的是host,冒号后是port,所以:
- host : www.cnblogs.com
- port : 8080

上面讲的都是URL中的参数,除了URL中的参数还有两个HTTP协议中常用的参数是:header (请求头)和body(常用于post请求中的请求提,有多种封装方法,不暴露在URL中)这两个参数。

综上:可以看出整个网络请求中的参数主要可以分为:scheme、authority、path、query、header、body 这6块。


2、Http中GET和POST的区别

HTTP定义了与服务器交互的不同方法,最基本的方法有四种,分别是GET、POST、PUT、DELETE。一个URL地址,它用于描述一个网络上的资源,而HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查,改,增,删4个操作。
1、GET请求: GET是向服务器发起索取数据的一种请求,用于获取信息而非修改信息;GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以 ? 分割 URL 和 传输数据,参数之间以 & 相连,例如:

login?name=hyddd&password=idontknow&verify=%E4%BD%A0%E5%A5;

%XX中的XX为该符号以16进制表示的ASCII,如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密

2、POST请求: POST是向服务器提交数据的一种请求,POST请求可能会导致新的资源的建立和/或已有资源的修改; POST把提交的数据则放置在是HTTP包的包体中。


3、Retrofit 概览

Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。这里没有说它是网络请求框架,主要原因在于网络请求的工作并不是 Retrofit 来完成的,因为Retrofit 2.0 开始内置 OkHttp,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作。

Retrofit时序图

应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,后者根据用户的需求对结果进行解析的过程。

3.1、Url配置

Retrofit 支持的协议包括 GET/POST/PUT/DELETE/HEAD/PATCH,这些协议均以注解的形式进行配置,例如GET的用法:

网络请求地址: gank.io/api/data/福利/10/1

public interface GirlsService {
    @GET("api/data/{type}/{count}/{page}")
    Call<ResponseBody> getGirls(@Path("type") String type, @Path("count") int count, @Path("page") int page);
    }

这些注解都有一个参数value,用来配置其路径,比如示例中的”api/data/{type}/{count}/{page}”,@GET中所填写的value和baseUrl组成一个完整 的路径,baseUrl会在构造Retrofit对象时给出,@GET注解中使用{type}、{count}、{page}声明了访问路径,可以把{type}、{count}、{page}当做占位符,实际运行中会通过@PATH(“type”)所标注的参数进行替换。

3.2、Retrofit注解

Retrofit通过使用注解来简化请求,大体分为以下几类:
1. 用于标注请求方式的注解
2. 用于标记请求头的注解
3. 用于标记请求参数的注解
4. 用于标记请求和响应格式的注解

1、请求方法注解

注解 说明
@GET get请求
@POST post请求
@PUT put请求
@DELETE delete请求
@PATCH patch请求,该请求是对put请求的补充,用于更新局部资源
@HEAD head请求
@OPTIONS option请求
@HTTP 通用注解,可以替换以上所有的注解,其拥有三个属性:method,path,hasBody

2、请求头注解

注解 说明
@Headers 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在
@Header 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头

3、请求参数注解

注解 说明
@Body 多用于post请求发送非表单数据,比如想要以post方式传递json格式数据
@Filed 多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用
@FiledMap 和@Filed作用一致,用于不确定表单参数
@Part 用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况
@PartMap 用于表单字段,默认接受的类型是 Map,可用于实现多文件上传
@Path 用于url中的占位符
@Query 用于Get中指定参数
@QueryMap 和Query使用类似
@Url 指定请求路径

4、请求和响应格式注解

注解 说明
@FormUrlEncoded 表示请求发送编码表单数据,每个键值对需要使用@Field注解
@Multipart 表示请求发送multipart数据,需要配合使用@Part
@Streaming 表示响应用字节流的形式返回,如果没使用该注解,默认会把数据全部载入到内存中,该注解在在下载大文件的特别有用

3.3、请求参数注解的使用方法

1、@Path:动态的url访问(这里只列举GET请求,其他请求一样处理)

网络请求地址: gank.io/api/data/An…

Path 分为带参数和不带参数两种情况:

不带参数的情况:

public interface GirlsService {
        @GET("api/data/Android/10/1")
        Call<ResponseBody> getGirls();
}

带参数的情况:

public interface GirlsService {
        @GET("api/data/{type}/{count}/{page}")
        Call<ResponseBody> getGirls(@Path("type") String type, @Path("count") int count, @Path("page") int page);
}

带参数就是在path路径中的type、count以及page是可以改变的,例如:gank.io/api/data/福利/10/1
1. 其中“福利”就是type,而且这个值还有其他选择,如Android、视频等;
2. 其中“10”就是count,表示每次请求的个数,可以随意替换;
3. 其中“1”就是page,代表页码,根据请求次数依次叠加;

2、@Query 和@QueryMap:查询参数的设置

网络请求地址 :api.github.com/users/whate…

@Query的使用方法:

public interface UserService {
        @GET("users/whatever")
        Call<ResponseBody> getUser(@Query("client_id") String id, @Query("client_secret") String secret)
}

访问代码:

Call<ResponseBody> call = retrofit.create(UserService.class).getUser("zhangsan", "12345@");

这样我们就完成了参数的指定,当然相同的方式也适用于POST,只需要把注解修改为@POST即可。

但是在实际的应用场景中,我们每次网络请求很少会只有一个查询参数,这时候就需要能够携带多个Query的@QueryMap:

@QueryMap的使用方法:

public interface UserService {
        @GET("users/whatever")
        Call<ResponseBody> getUser(@QueryMap Map<String, String> info)
}

访问代码:

    Map<String, String> params = new HashMap<>()
    params.put("client_id", "zhangsan");
    params.put("client_secret", "12345@");
    Call<ResponseBody> call = retrofit.create(UserService.class).getUser(params);

3、@Body:以POST请求体的方式向服务器上传 json 字符串

网络请求地址 :api.github.com/login

应用程序跟服务器通信,很多时候会选择直接使用POST方式将 json 字符串作为请求体发送到服务器,使用方法如下:

先定义个实体类User:

public class User{
    public String name;
    public String password;

    public User(String name, String password) {
        this.name= name;
        this.password= password;
    }

}

@Body的使用方法:

public interface LoginService{
         @POST("login")
         Call<ResponseBody> login(@Body User user);
}

访问代码:

     Call<ResponseBody> call =  retrofit.create(LoginService.class).login(new User("zhangsan", "12345@"))

其实就是使用@Body这个注解标识我们的参数对象即可。

但是我们平时并不会这样写,因为每次请求都要创建一个实体类,例如访问代码中的User,这样就会很麻烦,因为我们POST的是 json 字符串,所以我们可以这样写:

我们将@Body原来声明的参数User 改成了OKhttp中的RequestBody,并且使用 @Headers声明了请求头的参数类型:

public interface LoginService{
         @Headers({"Content-Type:application/json", "Accept:application/json"})
         @POST("login")
         Call<ResponseBody> login(@Body RequestBody body);
}

访问代码:

        JSONObject requestObject = new JSONObject();
        try {
            requestObject .put("client_id", "zhangsan");
            requestObject .put("client_secret", "12345@");
        } catch (Exception e) {
            e.printStackTrace();
        }
        RequestBody requestBody = RequestBody.create(okhttp3.MediaType.parse("application/json;charset=utf-8"), requestObject.toString());
        Call<ResponseBody> call =  retrofit.create(LoginService.class).login(requestBody);

首先创建一个JSONObject对象,然后将参数put到JSONObject对象中,再将这个JSONObject对象转换成我们需要的参数RequestBody ,然后就可以访问了。

4、@Field 和 @FieldMap:以表单的方式传递键值对 @FormUrlEncoded

网络请求地址 :api.github.com/login

其实我们用 POST 的场景相对较多,绝大多数的服务端接口都需要做加密、鉴权和校验,GET 显然不能很好的满足这个需求,使用 POST 提交表单的场景就更是刚需了;

定义一个方法:

public interface LoginService{
        @POST("login")
        @FormUrlEncoded
        Call<ResponseBody> login(@Field("username") String username, @Field("password") String password);
}

访问的代码:

    Call<ResponseBody> call = retrofit.create(LoginService.class).login("zhangsan", "12345@");

首先通过@POST指明Url,并且添加@FormUrlEncoded,然后通过@Field 声明了表单的项。

如果你不确定表单项的个数,还有能够携带多个Field的FieldMap:

定义一个方法:

    public interface LoginService{
        @FormUrlEncoded
        @POST("login")
        Call<ResponseBody> login(@FieldMap Map<String, String> map);
    }

访问的代码:

    Map<String, String> params = new ArrayMap<>()
    params.put("username", "zhangsan");
    params.put("password", "12345@");
    Call<ResponseBody> call = retrofit.create(LoginService.class).login(params);

5、@Part & 和 PartMap:文件上传 @Multipart

单文件上传方法:

    public interface FileUploadService {  
         @Multipart
         @POST("upload")
         Call<ResponseBody> upload(@Part("description") RequestBody description,
                               @Part MultipartBody.Part file);

         @Multipart
         @POST("register")
         Call<ResponseBody> register(@PartMap Map<String, RequestBody> params,  @Part("password") RequestBody password);
    }

单文件上传访问代码:

     //先创建 service
    FileUploadService service = retrofit.create(FileUploadService.class);
     //构建要上传的文件
     File file = new File(filename);
     RequestBody requestFile = RequestBody.create(MediaType.parse("application/otcet-stream"), file);
     MultipartBody.Part body =MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);

     String descriptionString = "This is a description";
     RequestBody description = RequestBody.create(MediaType.parse("multipart/form-data"), descriptionString);

     Call<ResponseBody> call = service.upload(description, body);
     call.enqueue(new Callback<ResponseBody>() {
       @Override
       public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
             System.out.println("success");
       }

       @Override
       public void onFailure(Call<ResponseBody> call, Throwable t) {
             t.printStackTrace();
       }
     });

多文件上传访问代码:

     //多文件上传
     File file = new File(Environment.getExternalStorageDirectory(), "messenger_01.png");
     RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
     Map<String, RequestBody> photos = new HashMap<>();
     photos.put("photos", photo);
     photos.put("username",  RequestBody.create(null, "abc"));

     Call<User> call = service.register(photos, RequestBody.create(null, "12345@"));

6、@Url :指定请求路径

网络请求地址: gank.io/api/data/An…

由于使用@Path 会存在 url 被转义的情况,但是使用 @Url 就不存在这个问题;

定义一个方法:

public interface ApiService {

        @GET
        Call<ResponseBody> getGirls(@Url String url);
}

访问的代码:

    Call<ResponseBody> call = retrofit.create(ApiService .class).getGirls("api/data/Android/10/1");

3.4、Converter.Factory 数据转换工厂

给Retrofit增加数据解析功能:

         Retrofit retrofit = new Retrofit.Builder()
              .baseUrl("http://gank.io/")
              .addConverterFactory(GsonConverterFactory.create())
              .build();

这里添加了gson转化工厂:addConverterFactory(GsonConverterFactory.create() ),Retrofit内部会根据这个转换工厂及返回数据所指定的泛型实现直接转换;Retrofit也支持自带如下格式:

  1. Gson: com.squareup.retrofit2:converter-gson
  2. Jackson: com.squareup.retrofit2:converter-jackson
  3. Moshi: com.squareup.retrofit2:converter-moshi
  4. Protobuf: com.squareup.retrofit2:converter-protobuf
  5. Wire: com.squareup.retrofit2:converter-wire
  6. Simple XML: com.squareup.retrofit2:converter-simplexml
  7. Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

4、使用Retrofit进行网络请求

我们以GET请求为例,实现一个完整的网络请求;

1、首先创建具体网络请求方法

public interface ApiService {

        @GET("api/data/{type}/{count}/{page}")
        Call<ResponseBody> getGirls(@Path("type") String type, @Path("count") int count, @Path("page") int page);

}

注意:如果 Call 的泛型指定的类不是ResponseBody,Retrofit会将返回的 string 用 json 转换器自动转换该类的一个对象,转换不成功就报错;如果不需要Gson转换,那么就指定泛型为ResponseBody,而且只能是ResponseBody,子类都不行。

2、然后创建一个Retrofit实例

  OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(3, TimeUnit.SECONDS)
                .writeTimeout(3, TimeUnit.SECONDS)
                .readTimeout(3, TimeUnit.SECONDS)
                .build();

  Retrofit retrofit = new Retrofit.Builder()
                 .baseUrl("http://gank.io/")
                 .addConverterFactory(GsonConverterFactory.create())
                 .client(client )
                 .build();

这里的baseUrl就是网络请求URL相对固定的地址,一般包括请求协议(如Http)、域名或IP地址、端口号等;还有addConverterFactory方法表示需要用什么转换器来解析返回值,GsonConverterFactory.create()表示调用Gson库来解析json返回值;client用于传入具体的OkHttpClient;

3、获取定义的接口实例

通过 Retrofit.create 就可以拿到我们定义的 ApiService 的实例,

  ApiService girlsService = retrofit.create(ApiService.class);

4、调用请求方法,并得到 Call 实例

  Call<ResponseBody> girlsCall = girlsService.getGirls("福利", size, page);

Call 其实在Retrofit中就是行使网络请求并处理返回值的类,调用的时候把需要拼接的参数传递进去,最后得到的完整Url地址为:

gank.io/api/data/福利/10/1

5、使用Call实例完成同步或异步请求

同步请求

Response<ResponseBody> response = girlsCall.execute().body();

需要注意的是同步网络请求一定要在子线程中完成,不能直接在UI线程执行,不然会crash;

异步请求

    girlsCall.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {

            }

            @Override
            public void onFailure(Call<GirlsBean> call, Throwable t) {

            }
     });

6、取消这个请求

Call 提供了cancel 方法可以取消请求,前提是该请求还没有执行;

girlsCall.cancel();  

5、Retrofit二次封装–Builder模式

4.1 Builder模式的定义

将一个复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的表示。

4.2 使用Builder的目的:

当我们请求网络的时候,需要一系列的参数,包括路径和请求参数,请求头,设置超时时间等等,考虑到以后可能会改,比如添加SSL判断,或者要使用gzip等,为了提高他的可拓展性,使用建造者模式可以让外部调用post方法以后,当内部逻辑改变时,不用去修改。直接在Builder类进行添加新的变量,并在post方法内部进行逻辑更改就好,外部使用者不会受到影响。

具体实现后的使用方法:

  private void getData() {
    HttpClient client = new HttpClient.Builder()
            .baseUrl("https://10.33.31.200:8890/")
            .url("msp/mobile/login")
            .params("username", "admin")
            .params("password", "B213CEAC2F1022023EF2699AA62599CF")
            .params("passwordLevel", "0")
            .params("captcha", "")
            .params("captchaKey", "")
            .params("loginAddr", "10.33.31.200")
            .params("mac", "6c:5c:14:8a:72:12")
            .build();
    client.get(new HttpClient.OnResultListener() {
        @Override
        public void onSuccess(String result) {
            Log.d("HttpClient", result);
        }

        @Override
        public void onFailure(String message) {
            Log.d("HttpClient", message);
        }
    });

4.3 具体的封装过程

先创建一个HttpClient类,然后创建一个Builder内部类,代码如下:
   public static final class Builder {
    private String baseUrl = BASE_URL;
    private String url;
    private Map<String, String> params = new ArrayMap<>();

    public Builder() {
    }

    public Builder baseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
        return this;
    }

    public Builder url(String url) {
        this.url = url;
        return this;
    }

    public Builder params(String key, String value) {
        this.params.put(key, value);
        return this;
    }

    public HttpClient build() {
        BASE_URL = baseUrl;
        HttpClient client = HttpClient.getIns();
        client.setBuilder(this);
        return client;
    }
}
给HttpClient创建私有的构造器,初始化OkHttpClient和Retrofit,代码如下:
    private HttpClient() {
        HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(MyApplication.getIntstance(), new int[0], R.raw.ivms8700, STORE_PASS);
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(7000, TimeUnit.MILLISECONDS)
            .sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)
            .hostnameVerifier(HttpsUtils.getHostnameVerifier())
            .build();

        retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build();
    }
建立POST和GET方法,代码如下:
public synchronized void get(final OnResultListener onResultListener) {
    Builder builder = mBuilder;
    if (!builder.params.isEmpty()) {
        String value = "";
        for (Map.Entry<String, String> entry : builder.params.entrySet()) {
            String mapKey = entry.getKey();
            String mapValue = entry.getValue();
            String span = value.equals("") ? "" : "&";
            String part = StringUtil.buffer(span, mapKey, "=", mapValue);
            value = StringUtil.buffer(value, part);
        }
        builder.url(StringUtil.buffer(builder.url, "?", value));
    }
    mCall = retrofit.create(HttpParams.class).params(builder.url);
    request(builder, onResultListener);
}

public synchronized void post(final OnResultListener onResultListener) {
    Builder builder = mBuilder;
    mCall = retrofit.create(HttpParams.class)
            .params(builder.url, builder.params);
    request(builder, onResultListener);
}
最后是正真去执行网络请求的方法,代码如下:
 private void request(final Builder builder, final OnResultListener onResultListener) {
    mCall.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            Log.d("HttpClient", "Call<ResponseBody> Request onSuccess=====>" + call.request());
            int code = response.code();
            if (code == 200) {
                String result = null;
                try {
                    result = response.body().string();
                    Log.d("HttpClient", "Http Response=====>" + result);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                onResultListener.onSuccess(result);
                return;
            }
            onResultListener.onFailure(response.toString());
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            Log.d("HttpClient", "Call<ResponseBody> Request onFailure=====>" + call.request());
            t.printStackTrace();
            onResultListener.onFailure(t.getMessage());
        }

    });
}
建一个接口,把请求网络的结果回调:
public interface OnResultListener {

    void onSuccess(String result);

    void onFailure(String message);
}
当然我们还要有传参的方法,service中通用方法的封装,下面是封装retrofit的service:

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

注意:如果方法的泛型指定的类不是ResonseBody,retrofit会将返回的string成用json转换器自动转换该类的一个对象,转换不成功就报错.

public interface HttpParams {

/**
 * POST方式将json字符串作为请求体发送到服务器
 * 其中@FormUrlEncoded 以表单的方式传递键值对
 * 其中 @Path:所有在网址中的参数(URL的问号前面)
 * 另外@FieldMap 用于POST请求,提交多个表单数据,@Field:用于POST请求,提交单个数据
 * 其中@Path(value = "filePath", encoded = true) String filePath是为了防止URL被转义为https://10.33.31.200:8890/msp%2Fmobile%2Flogin%3FloginAddr=10.33.31.200
 */
@FormUrlEncoded
@POST("{dataPath}")
Call<ResponseBody> params(@Path(value = "dataPath", encoded = true) String dataPath,
                          @FieldMap Map<String, String> param);


@GET
Call<ResponseBody> params(@Url String url);

@Multipart
@POST("upload")
Call<ResponseBody> upload(@Part("description") RequestBody description,
                          @Part MultipartBody.Part file);
}

这样一个retrofit的二次封装库算是做好了。

点击源码查看详细使用方法:HttpClient