Android-Retrofit详细学习

2017/7/30 posted in  Android

Retrofit入门

Retrofit 其实相当简单,简单到源码只有37个文件,其中22个文件是注解还都和HTTP有关,真正暴露给用户的类并不多。

创建Retrofit实例

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://localhost:4567/")
        .build();

创建Retrofit实例时需要通过Retrofit.Builder,并调用baseUrl方法设置URL。注: Retrofit2 的baseUlr 必须以 /(斜线) 结束,不然会抛出一个IllegalArgumentException

接口定义

以获取指定id的Blog为例:

public interface BlogService {
    @GET("blog/{id}")
    Call<ResponseBody> getBlog(@Path("id") int id);
}

注意,这里是interface不是class,所以我们是无法直接调用该方法,我们需要用Retrofit创建一个BlogService的代理对象。
BlogService service = retrofit.create(BlogService.class);
拿到代理对象之后,就可以调用该方法啦。

接口调用

Call<ResponseBody> call = service.getBlog(2);
// 用法和OkHttp的call如出一辙,
// 不同的是如果是Android系统回调方法执行在主线程
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        try {
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

打印结果:

{"code":200,"msg":"OK","data":{"id":2,"date":"2016-04-15 03:17:50","author":"怪盗kidou","title":"Retrofit2 测试2","content":"这里是 Retrofit2 Demo 测试服务器2"},"count":0,"page":0}

Retrofit注解详解

上面提到Retrofit 共22个注解,这节就专门介绍这22个注解,为帮助大家更好理解我将这22个注解分为三类,并用表格的形式展现出来,表格上说得并不完整,具体的见源码上的例子注释。

第一类:HTTP请求方法

20171012150781328418929.png
20171012150781328418929.png

以上表格中的除HTTP以外都对应了HTTP标准中的请求方法,而HTTP注解则可以代替以上方法中的任意一个注解,有3个属性:methodpathhasBody

public interface BlogService {
    /**
     * method 表示请求的方法,区分大小写
     * path表示路径
     * hasBody表示是否有请求体
     */
    @HTTP(method = "GET", path = "blog/{id}", hasBody = false)
    Call<ResponseBody> getBlog(@Path("id") int id);
}
public static void main(String[] args){
    Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://localhost:4567/")
        .build();
    BlogService service = retrofit.create(BlogService.class);
    Call<ResponseBody> call = service.getBlog(2);
    ResponseBodyPrinter.printResponseBody(call);
}

第二类:标记类

20171012150781333064912.png
20171012150781333064912.png

Field、FieldMap、Part和PartMap 示例

/**
 * [Retrofit注解详解 之 FormUrlEncoded/Field/FieldMap/Multipart/Part/PartMap注解]源码
 */
public class Example03 {
    public interface BlogService {

        /**
         * {@link FormUrlEncoded} 表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
         * <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);

        /**
         * Map的key作为表单的键
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded2(@FieldMap Map<String, Object> map);


        /**
         * {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
         * 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);

        /**
         * PartMap 注解支持一个Map作为参数,支持 {@link RequestBody } 类型,
         * 如果有其它的类型,会被{@link retrofit2.Converter}转换,如后面会介绍的 使用{@link com.google.gson.Gson} 的 {@link retrofit2.converter.gson.GsonRequestBodyConverter}
         * 所以{@link MultipartBody.Part} 就不适用了,所以文件只能用<b> @Part MultipartBody.Part </b>
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload2(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);

        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload3(@PartMap Map<String, RequestBody> args);
    }

    public static void main(String[] args) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://localhost:4567/")
                .build();

        BlogService service = retrofit.create(BlogService.class);


        // 演示 @FormUrlEncoded 和 @Field
        Call<ResponseBody> call1 = service.testFormUrlEncoded1("怪盗kidou", 24);
        ResponseBodyPrinter.printResponseBody(call1);


        //===================================================

        // 演示 @FormUrlEncoded 和 @FieldMap
        // 实现的效果与上面想同
        Map<String, Object> map = new HashMap<>();
        map.put("username", "怪盗kidou");
        map.put("age", 24);
        Call<ResponseBody> call2 = service.testFormUrlEncoded2(map);
        ResponseBodyPrinter.printResponseBody(call2);


        //===================================================


        MediaType textType = MediaType.parse("text/plain");
        RequestBody name = RequestBody.create(textType, "怪盗kidou");
        RequestBody age = RequestBody.create(textType, "24");
        RequestBody file = RequestBody.create(MediaType.parse("application/octet-stream"), "这里是模拟文件的内容");

        // 演示 @Multipart 和 @Part
        MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.txt", file);
        Call<ResponseBody> call3 = service.testFileUpload1(name, age, filePart);
        ResponseBodyPrinter.printResponseBody(call3);

        //===================================================
        // 演示 @Multipart 和 @PartMap
        // 实现和上面同样的效果
        Map<String, RequestBody> fileUpload2Args = new HashMap<>();
        fileUpload2Args.put("name", name);
        fileUpload2Args.put("age", age);
        //这里并不会被当成文件,因为没有文件名(包含在Content-Disposition请求头中),但上面的 filePart 有
        //fileUpload2Args.put("file", file);
        Call<ResponseBody> call4 = service.testFileUpload2(fileUpload2Args, filePart); //单独处理文件
        ResponseBodyPrinter.printResponseBody(call4);

        //===================================================
        // 还有一种比较hack的方式可以实现文件上传,
        // 上面说过被当成文件上传的必要条件就是 Content-Disposition 请求头中必须要有 filename="xxx" 才会被当成文件
        // 所有我们在写文件名的时候可以拼把 filename="XXX" 也拼接上去,
        // 即文件名变成  表单键名"; filename="文件名  (两端的引号会自动加,所以这里不加)也可以实现,但是不推荐方式

        Map<String, RequestBody> fileUpload3Args = new HashMap<>();
        fileUpload3Args.put("name",name);
        fileUpload3Args.put("age",age);
        fileUpload3Args.put("file\"; filename=\"test.txt",file);
        Call<ResponseBody> testFileUpload3 = service.testFileUpload3(fileUpload3Args);
        ResponseBodyPrinter.printResponseBody(testFileUpload3);
    }
}

第三类:参数类

20171012150781336489887.png
20171012150781336489887.png

注1:{占位符}和PATH尽量只用在URL的path部分,url中的参数使用QueryQueryMap 代替,保证接口定义的简洁注2:QueryFieldPart这三者都支持数组和实现了Iterable接口的类型,如ListSet等,方便向后台传递数组。

Call<ResponseBody> foo(@Query("ids[]") List<Integer> ids);
//结果:ids[]=0&ids[]=1&ids[]=2

Header和Headers例子

/**
 * [Retrofit注解详解 之 FormUrlEncoded/Field/FieldMap注解]源码
 */
public class Example04 {
    public interface BlogService {
        @GET("/headers?showAll=true")
        @Headers({"CustomHeader1: customHeaderValue1", "CustomHeader2: customHeaderValue2"})
        Call<ResponseBody> testHeader(@Header("CustomHeader3") String customHeaderValue3);

    }

    public static void main(String[] args) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://localhost:4567/")
                .build();

        BlogService service = retrofit.create(BlogService.class);

        //演示 @Headers 和 @Header
        Call<ResponseBody> call1 = service.testHeader("ikidou");
        ResponseBodyPrinter.printResponseBody(call1);
    }
}

Query、QueryMap、Url 示例

/**
 * [Retrofit注解详解 之 FormUrlEncoded/Field/FieldMap注解]源码
 */
public class Example05 {
    public interface BlogService {
        /**
         * 当GET、POST...HTTP等方法中没有设置Url时,则必须使用 {@link Url}提供
         * 对于Query和QueryMap,如果不是String(或Map的第二个泛型参数不是String)时
         * 会被默认会调用toString转换成String类型
         * Url支持的类型有 okhttp3.HttpUrl, String, java.net.URI, android.net.Uri
         * {@link retrofit2.http.QueryMap} 用法和{@link retrofit2.http.FieldMap} 用法一样,不再说明
         */
        @GET //当有URL注解时,这里的URL就省略了
        Call<ResponseBody> testUrlAndQuery(@Url String url, @Query("showAll") boolean showAll);

    }

    public static void main(String[] args) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://localhost:4567/")
                .build();

        BlogService service = retrofit.create(BlogService.class);

        //演示 @Headers 和 @Header
        Call<ResponseBody> call1 = service.testUrlAndQuery("headers",false);
        ResponseBodyPrinter.printResponseBody(call1);
    }
}

Gson与Converter

在默认情况下Retrofit只支持将HTTP的响应体转换换为ResponseBody,这也是什么我在前面的例子接口的返回值都是 Call<ResponseBody>,但如果响应体只是支持转换为ResponseBody的话何必要引用泛型呢,返回值直接用一个Call就行了嘛,既然支持泛型,那说明泛型参数可以是其它类型的,而Converter就是Retrofit为我们提供用于将ResponseBody转换为我们想要的类型,有了Converter之后我们就可以写把我们的第一个例子的接口写成这个样子了:

public interface BlogService {
  @GET("blog/{id}")
  Call<Result<Blog>> getBlog(@Path("id") int id);
}

当然只改变泛型的类型是不行的,我们在创建Retrofit时需要明确告知用于将ResponseBody转换我们泛型中的类型时需要使用的Converter引入Gson支持:
compile 'com.squareup.retrofit2:converter-gson:2.0.2'

通过GsonConverterFactoryRetrofit添加Gson支持:

Gson gson = new GsonBuilder()
      //配置你的Gson
      .setDateFormat("yyyy-MM-dd hh:mm:ss")
      .create();

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      //可以接收自定义的Gson,当然也可以不传
      .addConverterFactory(GsonConverterFactory.create(gson))
      .build();

这样Retrofit就会使用Gson将ResponseBody转换我们想要的类型。这是时候我们终于可以演示如使创建一个Blog了!

@POST("blog")
Call<Result<Blog>> createBlog(@Body Blog blog);

@Body注解的的Blog将会被Gson转换成RequestBody发送到服务器。

BlogService service = retrofit.create(BlogService.class);
Blog blog = new Blog();
blog.content = "新建的Blog";
blog.title = "测试";
blog.author = "怪盗kidou";
Call<Result<Blog>> call = service.createBlog(blog);

结果:

Result{code=200, msg='OK', data=Blog{id=20, date='2016-04-21 05:29:58', author='怪盗kidou', title='测试', content='新建的Blog'}, count=0, page=0}

RxJava与CallAdapter

说到Retrofit就不得说到另一个火到不行的库RxJava,网上已经不少文章讲如何与Retrofit结合,但这里还是会有一个RxJava的例子,不过这里主要目的是介绍使用CallAdapter所带来的效果。

第3节介绍的Converter是对于Call<T>T的转换,而CallAdapter则可以对Call转换,这样的话Call<T>中的Call也是可以被替换的,而返回值的类型就决定你后续的处理程序逻辑,同样Retrofit提供了多个CallAdapter,这里以RxJava的为例,用Observable代替Call

引入RxJava支持:

compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
// 针对rxjava2.x(adapter-rxjava2的版本要 >= 2.2.0)
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

通过RxJavaCallAdapterFactory为Retrofit添加RxJava支持:

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
      // 针对rxjava2.x
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 
      .build();

接口设计:

public interface BlogService {
  @POST("/blog")
  Observable<Result<List<Blog>>> getBlogs();
}

使用:

BlogService service = retrofit.create(BlogService.class);
service.getBlogs(1)
  .subscribeOn(Schedulers.io())
  .subscribe(new Subscriber<Result<List<Blog>>>() {
      @Override
      public void onCompleted() {
        System.out.println("onCompleted");
      }

      @Override
      public void onError(Throwable e) {
        System.err.println("onError");
      }

      @Override
      public void onNext(Result<List<Blog>> blogsResult) {
        System.out.println(blogsResult);
      }
  });

结果:

Result{code=200, msg='OK', data=[Blog{id=1, date='2016-04-15 03:17:50', author='怪盗kidou', title='Retrofit2 测试1', content='这里是 Retrofit2 Demo 测试服务器1'},.....], count=20, page=1}

像上面的这种情况最后我们无法获取到返回的Header和响应码的,如果我们需要这两者,提供两种方案:

  1. Observable<Response<T>> 代替 Observable<T> ,这里的Responseretrofit2.Response
  2. Observable<Result<T>> 代替 Observable<T>,这里的Result是指retrofit2.adapter.rxjava.Result,这个Result中包含了Response的实例。

自定义Converter

本节的内容是教大家实现在一简易的Converter,这里以返回格式为Call<String>为例。

在此之前先了解一下Converter接口及其作用:

public interface Converter<F, T> {
  // 实现从 F(rom) 到 T(o)的转换
  T convert(F value) throws IOException;

  // 用于向Retrofit提供相应Converter的工厂
  abstract class Factory {
    // 这里创建从ResponseBody其它类型的Converter,如果不能处理返回null
    // 主要用于对响应体的处理
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
    Retrofit retrofit) {
      return null;
    }

    // 在这里创建 从自定类型到ResponseBody 的Converter,不能处理就返回null,
    // 主要用于对Part、PartMap、Body注解的处理
    public Converter<?, RequestBody> requestBodyConverter(Type type,
    Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return null;
    }

    // 这里用于对Field、FieldMap、Header、Path、Query、QueryMap注解的处理
    // Retrfofit对于上面的几个注解默认使用的是调用toString方法
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
    Retrofit retrofit) {
      return null;
    }

  }
}

我们要想从Call<ResponseBody> 转换为 Call<String> 那么对应的F和T则分别对应ResponseBodyString,我们定义一个StringConverter并实现Converter接口。

public static class StringConverter implements Converter<ResponseBody, String> {

  public static final StringConverter INSTANCE = new StringConverter();

  @Override
  public String convert(ResponseBody value) throws IOException {
    return value.string();
  }
}

我们需要一个Fractory来向Retrofit注册StringConverter

public static class StringConverterFactory extends Converter.Factory {

  public static final StringConverterFactory INSTANCE = new StringConverterFactory();

  public static StringConverterFactory create() {
    return INSTANCE;
  }

  // 我们只关实现从ResponseBody 到 String 的转换,所以其它方法可不覆盖
  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
    if (type == String.class) {
      return StringConverter.INSTANCE;
    }
    //其它类型我们不处理,返回null就行
    return null;
  }
}

使用Retrofit.Builder.addConverterFactory向Retrofit注册我们StringConverterFactory

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      // 如是有Gson这类的Converter 一定要放在其它前面
      .addConverterFactory(StringConverterFactory.create())
      .addConverterFactory(GsonConverterFactory.create())
      .build();

注:addConverterFactory是有先后顺序的,如果有多个ConverterFactory都支持同一种类型,那么就是只有第一个才会被使用,而GsonConverterFactory是不判断是否支持的,所以这里交换了顺序还会有一个异常抛出,原因是类型不匹配。

只要返回值类型的泛型参数就会由我们的StringConverter处理,不管是Call<String>还是Observable<String>有没有很简单?如果你有其它的需求处理的就自己实现吧。

自定义CallAdapter

本节将介绍如何自定一个CallAdapter,并验证是否所有的String都会使用我们第5节中自定义的Converter。

先看一下CallAdapter接口定义及各方法的作用:

public interface CallAdapter<T> {

  // 直正数据的类型 如Call<T> 中的 T
  // 这个 T 会作为Converter.Factory.responseBodyConverter 的第一个参数
  // 可以参照上面的自定义Converter
  Type responseType();

  <R> T adapt(Call<R> call);

  // 用于向Retrofit提供CallAdapter的工厂类
  abstract class Factory {
    // 在这个方法中判断是否是我们支持的类型,returnType 即Call<Requestbody>和`Observable<Requestbody>`
    // RxJavaCallAdapterFactory 就是判断returnType是不是Observable<?> 类型
    // 不支持时返回null
    public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations,
    Retrofit retrofit);

    // 用于获取泛型的参数 如 Call<Requestbody> 中 Requestbody
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    // 用于获取泛型的原始类型 如 Call<Requestbody> 中的 Call
    // 上面的get方法需要使用该方法。
    protected static Class<?> getRawType(Type type) {
      return Utils.getRawType(type);
    }
  }
}

了解了CallAdapter的结构和其作用之后,我们就可以开始自定义我们的CallAdapter了,本节以CustomCall<String>为例。
在此我们需要定义一个CustomCall,不过这里的CustomCall作为演示只是对Call的一个包装,并没有实际的用途。

public static class CustomCall<R> {

  public final Call<R> call;

  public CustomCall(Call<R> call) {
    this.call = call;
  }

  public R get() throws IOException {
    return call.execute().body();
  }
}

有了CustomCall,我们还需要一个CustomCallAdapter来实现 Call<T> 到 CustomCall<T>的转换,这里需要注意的是最后的泛型,是我们要返回的类型。

public static class CustomCallAdapter implements CallAdapter<CustomCall<?>> {

  private final Type responseType;

  // 下面的 responseType 方法需要数据的类型
  CustomCallAdapter(Type responseType) {
    this.responseType = responseType;
  }

  @Override
  public Type responseType() {
    return responseType;
  }

  @Override
  public <R> CustomCall<R> adapt(Call<R> call) {
    // 由 CustomCall 决定如何使用
    return new CustomCall<>(call);
  }
}

提供一个CustomCallAdapterFactory用于向Retrofit提供CustomCallAdapter

public static class CustomCallAdapterFactory extends CallAdapter.Factory {
  public static final CustomCallAdapterFactory INSTANCE = new CustomCallAdapterFactory();

  @Override
  public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    // 获取原始类型
    Class<?> rawType = getRawType(returnType);
    // 返回值必须是CustomCall并且带有泛型
    if (rawType == CustomCall.class && returnType instanceof ParameterizedType) {
      Type callReturnType = getParameterUpperBound(0, (ParameterizedType) returnType);
      return new CustomCallAdapter(callReturnType);
    }
    return null;
  }
}

使用addCallAdapterFactory向Retrofit注册CustomCallAdapterFactory

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      .addConverterFactory(Example09.StringConverterFactory.create())
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(CustomCallAdapterFactory.INSTANCE)
      .build();

注: addCallAdapterFactoryaddConverterFactory同理,也有先后顺序。

其它说明

Retrofit.Builder

前面用到了 Retrofit.Builder 中的baseUrladdCallAdapterFactoryaddConverterFactorybuild方法,还有callbackExecutorcallFactoryclientvalidateEagerly这四个方法没有用到,这里简单的介绍一下。

方法 用途
callbackExecutor(Executor) 指定Call.enqueue时使用的Executor,所以该设置只对返回值为Call的方法有效
callFactory(Factory) 设置一个自定义的okhttp3.Call.Factory,那什么是Factory呢?OkHttpClient就实现了okhttp3.Call.Factory接口,下面的client(OkHttpClient)最终也是调用了该方法,也就是说两者不能共用
client(OkHttpClient) 设置自定义的OkHttpClient,以前的Retrofit版本中不同的Retrofit对象共用同OkHttpClient,在2.0各对象各自持有不同的OkHttpClient实例,所以当你需要共用OkHttpClient或需要自定义时则可以使用该方法,如:处理Cookie、使用 stetho 调式等
validateEagerly(boolean) 是否在调用create(Class)时检测接口定义是否正确,而不是在调用方法才检测,适合在开发、测试时使用

Retrofit的Url组合规则

BaseUrl 和URL有关的注解中提供的值 最后结果
http://localhost:4567/path/to/other/ /post http://localhost:4567/post
http://localhost:4567/path/to/other/ post http://localhost:4567/path/to/other/post
http://localhost:4567/path/to/other/ https://github.com/ikidou https://github.com/ikidou

从上面不能难看出以下规则:

  • 如果你在注解中提供的url是完整的url,则url将作为请求的url。
  • 如果你在注解中提供的url是不完整的url,且不以 / 开头,则请求的url为baseUrl+注解中提供的值
  • 如果你在注解中提供的url是不完整的url,且以 / 开头,则请求的url为baseUrl的主机部分+注解中提供的值

Retrofit提供的Converter

Converter Gradle依赖
Gson com.squareup.retrofit2:converter-gson:2.0.2
Jackson com.squareup.retrofit2:converter-jackson:2.0.2
Moshi com.squareup.retrofit2:converter-moshi:2.0.2
Protobuf com.squareup.retrofit2:converter-protobuf:2.0.2
Wire com.squareup.retrofit2:converter-wire:2.0.2
Simple XML com.squareup.retrofit2:converter-simplexml:2.0.2
Scalars com.squareup.retrofit2:converter-scalars:2.0.2

Retrofit提供的CallAdapter:

CallAdapter Gradle依赖
guava com.squareup.retrofit2:adapter-guava:2.0.2
Java8 com.squareup.retrofit2:adapter-java8:2.0.2
rxjava com.squareup.retrofit2:adapter-rxjava:2.0.2