用 Retrofit 2 简化 HTTP 请求-来自 Droidcon NYC 2015 一个演讲

Retrofit 作为简化 HTTP 请求的库,已经运行多年,2.0版本依然不辱使命的在做这些事情。不过 2.0 版本修复了一些长期影响开发者的设计,还加入了前所未有的强大特性。在 NYC 2015 的这一个分享中,Jake Wharton 的演讲涵盖了所有 Retrofit 2.0 的新特性,全面介绍了 Retrofit 2.0 工作原理。

Transcription below provided by Realm: a replacement for SQLite that you can use in Java or Kotlin. Check out the docs!

Jake Wharton 是工作在 Square 的工程师。过去五年一直在跟糟糕的代码和 API 作斗争。经常参加各类会议,讨论这种影响千万开发者的如同瘟疫般的设计。

@jakewharton



Save the date for Droidcon SF in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.


简介 (0:00)

我叫 Jake Wharton,现在在 Square 工作。一个天真的人曾经说过:”Retrofit 2 将会在今年年底前放出。”,那个人,就是去年在纽约 DroidCon 上表态的我。然而,事实是 Retrofit 2 将会在今年年底放出,这次我保证!

Retrofit 5年前就开源了,是 Square 最早的开源项目之一。一开始的时候,Retrofit 只是我们用在各个开源项目里的福袋:比如说最早里面有晃动检测功能,HTTP Client,还有现在的 tap 库。多数功能都是 Bob Lee 完成的,我大概 3 年前开始接管这些工作。最终历经 3 年,完成了 1.0 版本,然后彻底开源。从那会儿到现在,已经 release 了 18 个版本了。

Retrofit 1 不错的地方 (2:23)

Retrofit 里已经有很多不错的特性了。Retrofit 可以利用接口,方法和注解参数(parameter annotations)来声明式定义一个请求应该如何被创建。比如说,下面是一个如何请求 GitHub API 的例子:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

Retrofit 背后的 HTTP client,以及序列化机制(JSON/XML 协议)都是可替换(pluggable)的,因此你可以选择合适自己的方案。Retrofit 最早出来的时候,只支持 Apache 的 HTTP client。在 1.0 放出前,我们增加了 URL connection,以及 OkHttp 的支持。如果你想要加入的其他的 HTTP client,都可以简单的加入。这个特性非常赞,让我们有能力去支持不同的自定义 client。

builder.setClient(new UrlConnectionClient());
builder.setClient(new ApacheClient());
builder.setClient(new OkClient());
builder.setClient(new CustomClient());

序列化功能也是可替换的。默认是用的 GSON,你当然也可以用 Jackson 来替换掉。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
builder.setConverter(new GsonConverter());
builder.setConverter(new JacksonConverter());

如果你在用某些数据交换协议,比如 protocol buffer,Retrofit 也支持 Google 的 protobuf,也包括 XML 协议的转换(如果你自己不怕折腾)。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  ContributorResponse repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
builder.setConverter(new ProtoConverter());
builder.setConverter(new WireConverter());
builder.setConverter(new SimpleXMLConverter());
builder.setConverter(new CustomConverter());

序列化部分跟 client 部分一样,都是可替换的。你如果想要引入或者实现自己的序列化组件,完全没有问题。

在发请求的实现上,你可以用的方法有很多,比如:同步发送请求——

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
List<Contributor> contributors =
    gitHubService.repoContributors("square", "retrofit");

——和异步发送,他们之间的区别就是异步发送要在最后一个参数上声明一个 callback 回调函数:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  void repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo,
      Callback<List<Contributor>> cb);
}
service.repoContributors("square", "retrofit", new Callback<List<Contributor>>() {
  @Override void success(List<Contributor> contributors, Response response) {
    // ...
  }
  @Override void failure(RetrofitError error) {
    // ...
  }
});

——再到后来 1.0 后,我们还支持了 RxJava ,被证明真的是个非常受欢迎的功能。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
gitHubService.repoContributors("square", "retrofit")
    .subscribe(new Action1<List<Contributor>>() {
      @Override public void call(List<Contributor> contributors) {
        // ...
      }
    });

Retrofit 1: 不够好的地方 (4:58)

不幸的是,没有一个库是完美的,Retrofit 也不例外。为了支持可替换的功能模块,我们必须嵌套大量的组件,类的数量极多以至于成为了一个痛处,一方面是因为整个库非常的脆弱,还有就是因为我们无法修改公开的 API 接口。

如果你想要操作某次请求返回的数据,比如说返回的 Header 部分或者 URL,你又同时想要操作序列化后的数据部分,这是 Retrofit 1.0 上是不可能实现的。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
  @GET("/repos/{owner}/{repo}/contributors")
  Response repoContributors2(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

在上面的这个 GitHub 的例子里,我们返回了一个 contributor 的列表,你可以用不同的 converter 去做反序列化。然而,如果说你要读取一个 reponse 的 header 部分。除非你设置一个 endpoint 来接管这个 reponse,不然你没有办法去读取这个 response。 由于 response header 数据里并没有反序列化后的对象,如果不做反序列化操作的话,那你也就无法拿到 contributor 对象了。

我刚才说过同步和异步,以及用起来非常棒的 RxJava,但是这些用起来却有些刻板。比如:我们在某些场景下既需要异步的调用,又需要同步的调用。在 Retrofit 1.0 里,你必须得声明两次这个方法,像下面这样:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  List<Contributor> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
  @GET("/repos/{owner}/{repo}/contributors")
  void repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo,
      Callback<List<Contributor>> cb);
}

RxJava 也有类似问题。但值得庆幸的是你在用 RxJava 的时候只用声明一次就行,为了实现这个,我们还在核心代码里增加了对 RxJava 的支持,以辅助返回 Observable 的对象。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

我们可能已经熟悉如何在 Retrofit 里创建 Observable 对象。但是如果你需要一些其他对象呢? 比如,我们没有计划支持用了 Guava 的 ListenableFuture,以及那些用了 Java 8 的 CompleteableFuture。毕竟,Retrofit 1 是基于还在用着 Java 6 的 Android 开发的。

Retrofit 1 里 Converter 工作的效率并不算是很高。下面是在 Retrofit 1 里创建自定义 Converter 的代码,非常简单:

interface Converter {
  Object fromBody(TypedInput body, Type type);
  TypedOutput toBody(Object object);
}

自定义 Converter 接收一个对象,然后返回一个格式化后的 HTTP 对象。问题是在我们传入了 Response 和一个我们想要转换的格式 Type 参数后,Converter 必须得搞清楚到底应该如何去反序列化,这部分的实现很复杂,而且耗时。尽管一些库做了对象的缓存,但依然效率很低。

interface GitHubService {
  @GET("/search/repositories")
  RepositoriesResponse searchRepos(
      @Query("q") String query,
      @Query("since") Date since);
}
/search/repositories?q=retrofit&since=2015-08-27
/search/repositories?q=retrofit&since=20150827

有时候,声明式 API 会遇到一些小问题。比如就像上面的例子一样,你有个接口需要传入一个 Date,但是一个 Date 会有多种不同的格式表示。有的接口可能需要一个字符串,有的可能需要一个分隔开的日期表示(尤其是那些比日期要复杂很多的对象,可能会有更多的表示方法)。

以上,基本上就是 Retrofit 1 无力解决的需求了,我们要如何修复呢?

Retrofit 2 (10:18)

开发 Retrofit2 的时候,我们希望我们定位和解决所有大家多年以来在 Retrofit 1 里遇到的那些问题。

Call (10:30)

首先得提到的是:Retrofit2 有了新的类型。如果你熟悉用 OkHttp 做 API 请求,你可能比较熟悉其中的一个类:Call。现在, Retrofit 2 里也多了一个 call 方法。语法和 OkHttp 基本一模一样,唯一不同是这个函数知道如何做数据的反序列化。它知道如何将 HTTP 响应转换成对象。

另外,每一个 call 对象实例只能被用一次,所以说 request 和 response 都是一一对应的。你其实可以通过 Clone 方法来创建一个一模一样的实例,这个开销是很小的。比如说:你可以在每次决定发请求前 clone 一个之前的实例。

另一个大的进步是 2.0 同时支持了在一个类型中的同步和异步。同时,一个请求也可以被真正地终止。终止操作会对底层的 http client 执行 cancel 操作。即便是正在执行的请求,也能立即切断。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");

这个 Call 对象是从你的 API 接口返回的参数化后的对象。调用跟接口名相同的函数名,你就会得到一个实例出来。我们可以直接调用它的 execute 方法,但是得留意一下,这个方法只能调用一次。

Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");
response = call.execute();
// This will throw IllegalStateException:
response = call.execute();
Call<List<Contributor>> call2 = call.clone();
// This will not throw:
response = call2.execute();

当你尝试调用第二次的时候,就会出现失败的错误。实际上,可以直接克隆一个实例,代价非常低。当你想要多次请求一个接口的时候,直接用 clone 的方法来生产一个新的,相同的可用对象吧。

想要实现异步,需要调用 enqueue 方法。现在,我们就能通过一次声明实现同步和异步了!

Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");
call.enqueue(new Callback<List<Contributor>>() {
  @Override void onResponse(/* ... */) {
    // ...
  }
  @Override void onFailure(Throwable t) {
    // ...
  }
});

当你将一些异步请求压入队列后,甚至你在执行同步请求的时候,你可以随时调用 cancel 方法来取消请求:

Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");
call.enqueue(         );
// or...
call.execute();
// later...
call.cancel();

Parameterized Response Object (13:48)

另一个新的特性是参数化的 Response 类型。 Response 对象增加了曾经一直被我们忽略掉的重要元数据:响应码(the reponse code),响应消息(the response message),以及读取相应头(headers)。

class Response<T> {
  int code();
  String message();
  Headers headers();
  boolean isSuccess();
  T body();
  ResponseBody errorBody();
  com.squareup.okhttp.Response raw();
}

同时还提供了一个很方便的函数来帮助你判断请求是否成功完成,其实就是检查了下响应码是不是 200。然后就拿到了响应的 body 部分,另外有一个单独的方法获取 error body。基本上就是出现一个返回码,然后调用相对应的函数的。只有当响应成功以后,我们会去做反序列化操作,然后将反序列化的结果放到 body 回调中去。如果出现了返回了网络成功响应(返回码:200)却最终返回 false 的情况,我们实际上是无法判断返回到底是什么的,只能将 ResponseBody(简单封装的了 content-type,length,以及 raw body部分) 类型交给你去处理。

以上是在声明接口时候的两个重大的改变。

动态 URL Parameter (16:33)

动态 URL 参数是让我头疼多年的一个问题,现在我们终于解决了!如果你向 GitHub 发出多个请求,收到一个响应,通常这个响应大概像下面这样:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
Call<List<Contributor>> call =
    gitHubService.repoContributors("square", "retrofit");
Response<List<Contributor>> response = call.execute();
// HTTP/1.1 200 OK
// Link: <https://api.github.com/repositories/892275/contributors?
page=2>; rel="next", <https://api.github.com/repositories/892275/
contributors?page=3>; rel="last"
// ...

要是你想做分页,你就得自己去分析这些 URL 了。GitHub 可能将 header link 地址列表里的数据已经缓存在服务器内存里了,当你去按他们指引的地址去请求的话,他们就不必费劲去从数据库里给你拿数据了,速度上也更快。但是,在 Retrofit 1.0 的时候,我们没有办法去直接执行 GitHub Server 返回在 header 里的请求地址。

用上我们新的 response 类型后,不止是我刚才提到的那些元数据,我们还可以写一些方法来读出自定义的字段,比如上面例子里的下一页的地址:

Response<List<Contributor>> response = call.execute();
// HTTP/1.1 200 OK
// Link: <https://api.github.com/repositories/892275/contributors?
page=2>; rel="next", <https://api.github.com/repositories/892275/
contributors?page=3>; rel="last"
// ...
String links = response.headers().get("Link");
String nextLink = nextFromGitHubLinks(links);
// https://api.github.com/repositories/892275/contributors?page=2

这个可能和上面的接口生成地址略有不同。

动态 URL 地址就是用在连续请求里的。在第一个请求之后,如果返回的结果里有指明下个请求的地址的话,在之前,你可能得单独写个 interface 来处理这种情况,现在就无需那么费事了。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
  @GET
  Call<List<Contributor>> repoContributorsPaginate(
      @Url String url);
}

Retrofit 2.0 有了新的 标注:@Url ,允许你直接传入一个请求的 URL。

有了这个方法后,我们就可以直接把刚才取出来的下一页的地址传入,是不是一切都流畅了很多:

String nextLink = nextFromGitHubLinks(links);
// https://api.github.com/repositories/892275/contributors?page=2
Call<List<Contributor>> nextCall =
    gitHubService.repoContributorsPaginate(nextLink);

这样的话,我们就能通过调用 repoContributorsPaginate 来获取第二页内容,然后通过第二页的 header 来请求第三页。你可能很多的 API 都见到过类似的设计,这在 Retrofit 1 里确实是个困扰很多人的大麻烦。

更多更有效的 Converters (19:31)

Retrofit 1 里有一个 converter 的问题。多数人可能没遇到过,是库内部的一个问题。在 Retrofit 2 里,我们已经解决了这个问题,同时开始支持多种 Converter 并存。

在之前,如果你遇到这种情况:一个 API 请求返回的结果需要通过 JSON 反序列化,另一个 API 请求需要通过 proto 反序列化,唯一的解决方案就是将两个接口分离开声明。

interface SomeProtoService {
  @GET("/some/proto/endpoint")
  Call<SomeProtoResponse> someProtoEndpoint();
}
interface SomeJsonService {
  @GET("/some/json/endpoint")
  Call<SomeJsonResponse> someJsonEndpoint();

之所以搞得这么麻烦是因为一个 REST adapter 只能绑定一个 Converter 对象。我们费工夫去解决这个是因为:接口的声明是要语意化的。API 接口应该通过功能实现分组,比如: account 的接口,user 的接口,或者 Twitter 相关的接口。返回格式的差异不应该成为你分组时候的阻碍。

现在,你可以把他们都放在一起了:

interface SomeService {
  @GET("/some/proto/endpoint")
  Call<SomeProtoResponse> someProtoEndpoint();
  @GET("/some/json/endpoint")
  Call<SomeJsonResponse> someJsonEndpoint();
}

我大概提一下这个是怎么工作起来的,因为了解 Converter 的调用原理在写代码的时候很重要。

看我们上面的代码:第一个方法返回了一个 proto 对象。

SomeProtoResponse —> Proto? Yes!

原理很简单,其实就是对着每一个 converter 询问他们是否能够处理某种类型。我们问 proto 的 converter: “Hi, 你能处理 SomeProtoResponse 吗?”,然后它尽可能的去判断它是否可以处理这种类型。我们都知道:Protobuff 都是从一个名叫 message 或者 message lite 的类继承而来。所以,判断方法通常就是检查这个类是否继承自 message。

在面对 JSON 类型的时候,首先问 proto converter,proto converter 会发现这个不是继承子 Message 的,然后回复 no。紧接着移到下一个 JSON converter 上。JSON Converter 会回复说我可以!

SomeJsonResponse —> Proto? No! —> JSON? Yes!

因为 JSON 并没有什么继承上的约束。所以我们无法通过什么确切的条件来判断一个对象是否是 JSON 对象。以至于 JSON 的 converters 会对任何数据都回复说:我可以处理!这个一定要记住, JSON converter 一定要放在最后,不然会和你的预期不符。

另一个要注意的是,现在已经不提供默认的 converter 了。如果不显性的声明一个可用的 Converter 的话,Retrofit 是会报错的:提醒你没有可用的 Converter。因为核心代码已经不依赖序列化相关的第三方库了,我们依然提供对 Converter 的支持,不过你需要自己引入这些依赖,同时显性的声明 Retrofit 需要用的 Converter 有哪些。

更多可替换的执行机制 (22:38)

在此之前,Retrofit 有一个死板的 execution 流程。在 Retrofit 2 里,我们调整了整个流程,让它变得可替换(pluggable),同时允许多个。跟 converter 的工作原理很像。

比如说,你有一个方法返回了一个 Call 对象,Call 是内置的 Converter 类型。比如:Retrofit 2 的执行机制:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
...

现在,你可以自定义这些了。或者用我们提供的一个:

...
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors2(
      @Path("owner") String owner,
      @Path("repo") String repo);
  @GET("/repos/{owner}/{repo}/contributors")
  Future<List<Contributor>> repoContributors3(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

Retrofit 2.0 依然支持 RxJava,但现在是分离的。(你如果想要一些别的特性,你也可以自己写一个)同时支持不同的 execution 是怎么实现的呢?

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors2(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Future<List<Contributor>> repoContributors3(..);
}

通过返回类型来判断需要调用哪个 exection。比如说:返回为 Call 的类型, 我们的整个执行机制会问:“Hey,你知不知道如何处理 Call ?” 如果是 RxJava,它就会说:“我不知道,我只知道 Observable 的处理方法。”。 随后,我们又问内部的 converter,他刚好回答说:“是的!我会!”。

call —> RxJava? No! —> Call? Yes!

Observable 也是同样的工作原理。我们同样问 RxJava,它就说:“我能处理这个”:

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors2(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Future<List<Contributor>> repoContributors3(..);
}

Observable —> RxJava? Yes!

如果你没装相对应的 Converter,这就意味着我们无法验证响应的类型。比如:如果询问是否有办法能处理 Future,他们两个都会说:“不行”。

interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Observable<List<Contributor>> repoContributors2(..);
  @GET("/repos/{owner}/{repo}/contributors")
  Future<List<Contributor>> repoContributors3(..);
}

Future —> RxJava? No! —> Call? No! —> Throw!

这将会返回一个 Exception,意味着这两个以及内置的机制都无法处理这种类型。关于工作原理我们随后会深入讨论。

OkHttp 提供支持 (24:17).

Retrofit 2 现在开始依赖了 OkHttp 了,而且这部分不再支持替换。这是一件比较有争议的事情。但是希望我能证明为什么这是一个对的决定。

OkHttp 现在很小而且很聚焦,有很多好用的 API 接口。我们在 Retrofit 2 里都有对 OkHttp 的接口映射,也基本具备了我们需要的所有的特性,包括提到的所有的抽象类们。这些都超赞!这是压缩 Retrofit 库大小的一个法宝。我们最终减小了Retrofit 60% 的体积,同时又具有了更多的特性。

OkHttp 提供支持 (以及 Okio!) (26:20)

另一个用 OkHttp 的好处就是我们能够在 Retrofit 2 把 OkHttp 以公开接口的方式直接导出。你可能在 error body 方法或者响应里见到过 response body 的内容。显然,我们在 Retrofit 的 Reponse 对象里直接返回了 OkHttp 的 Response body。我们正在导出这些类型,OkHttp 的类型基本上已经以更好更简洁的 API 替代 Retrofit 1.0 的一些接口。

OkHttp 的背后是一个叫做 Okio 的库,提供的 IO 支持。我之前在 Droidcon Montreal 做过关于这个库的演讲。讨论过为什么它是众多 IO 库中更好的选择,还讨论了它为何极度高效,以及为什么你应该使用它们。演讲中我还提到 Retrofit 2 ,当时它还是脑海里的一个概念。现在 Retrofit 2 已经实现了。

Retrofit 2 的效率 (27:31)

我做了这个图表来展示 Retrofit 相比 Retrofit 1 以及其他可能的方案要高效的多,这归功于刚刚提到的硬性依赖和那些抽象。我带大家来看一下上面视频中的这个表。所以一定要看我演讲的这部分噢!

初始化 - Retrofit 类型 (31:24)

现在,让我们来看一下 Retrofit 的类型是如何替代 REST adapter 类型的,以及如何初始化。原来的方法叫做 endpoint, 不过现在我们称之为 baseUrl, baseUrl 就是你所请求的 Server 的 URL,下面是一个请求 GitHub Api 的例子:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .build();
interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
GitHubService gitHubService = retrofit.create(GitHubService.class);

我们声明了自己的接口,我们称作创建方法,跟 Retrofit 1 里的是一致的。接下来,我们来生成一个接口的实现,以使这些接口方法可以直接被调用。

当我们调用 repoContributors 这个方法的时候,Retrofit 会创建这个 URL。如果我们传入 SquareRetrofit 字符串,分别作为 ownerrepo 参数。我们就会得到这个 URL:https://api.github.com/repos/square/retrofit/contributors。在 Retrofit 内部,Retrofit 会用 OkHttp 的 HTTP URL 类型作为 基础的 URL ,然后 resolve 方法就会取出相对地址和 baseUrl 拼接起来,接着发起请求。接下来给你展示下改变 API 前缀,比如 V3。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/v3/")
    .build();
interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

虽然说这不是 GitHub 真实的 API,但是真实世界里就是有很多 API 是由这样的前缀和路径组成的。调用相同的方法,被解析出来的 URL 将会是是这样的: https://api.github.com/repos/square/retrofit/contributors。可以看到在主机地址之后并没有v3 ,这是因为地址的 URL 是以一个斜线开始的,而在 HTTP 里,斜线开始的地址往往是绝对地址后缀路径。Retrofit 1 会因为语义化的约束,强制你加这个前缀斜线, 然后把 baseUrl 和相对地址拼接起来。现在,考虑到规范问题,我们已经对这两种地址加以区分。

interface GitHubService {
  @GET("repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

如果有前缀 / 就代表着是一个绝对路径。删除了那个前缀的 /, 你将会得到正确的、包含了 v3 路径的全 URL。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/v3/")
    .build();
interface GitHubService {
  @GET("repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> repoContributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}
// https://api.github.com/v3/repos/square/retrofit/contributors

由于现在我们开始依赖 OkHttp, 并没有 Http Client 层的抽象。现在是可以传递一个配置好的 OkHttp 实例的。比如:配置 interceptors, 或者一个 SSL socket 工厂类, 或者 timeouts 的具体数值。 (OkHttp 有默认的超时机制,如果你不需要自定义,实际上不必进行任何设置,但是如果你想要去设置它们,下面是一个例子告诉你来怎么操作。)

OkHttpClient client = new OkHttpClient();
client.interceptors().add(..);
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .client(client)
    .build();

如果你要指明特定的 converter 或者 execute 机制,也是在这个时候加的。比如这会儿:我们可以给 GSON 设置一个或者多个 converter。也可以给 protocol buffer 设置一个 converter。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .addConverterFactory(GsonConverterFactory.create())
    .addConverterFactory(ProtoConverterFactory.create())
    .build();

我想要强调的是:添加 converter 的顺序很重要。按照这个顺序,我们将依次询问每一个 converter 能否处理一个类型。我上面写的其实是错的。如果我们试图反序列化一个 proto 格式,它其实会被当做 JSON 来对待。这显然不是我们想要的。我们需要调整下顺序,因为我们先要检查 proto buffer 格式,然后才是 JSON。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .addConverterFactory(ProtoConverterFactory.create())
    .addConverterFactory(GsonConverterFactory.create())
    .build();

Retrofit 的文档里可能还没这些,如果你想要使用 RxJava 来代替 call, 你需要一个 Call Adapter Factory:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .addConverterFactory(ProtoConverterFactory.create())
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

Call Adapter Factory 是一个知道如何将 call 实例转换成其他类型的工厂类。目前,我们只有 RxJava 的类型,也就是将 Call 类型转换成 Observable 类型。如果你了解 RxJava, 其实还有一种新的 Observable 类型(一次只发射一个 item 的类型)。你可以用这个 call adapter factory 来转换到其中任意一种 Observable。

扩展性 (36:50)

刚才提到的 Factory,也是可扩展的。这意味着你可以写属于自己的 Call Adapter Facotry。实现起来其实就是一个方法。传递给它一个类型,返回 null 代表拒绝,或者返回一个 converter 的实例。

SomeJsonResponse
		class ProtoConverterFactory {
  			Converter<?> create(Type type);		null
		}
		class GsonConverterFactory {
			Converter<?> create(Type type);		Converter<?>
		}

看上面的例子,如果你给它传递一个 JSON 的 Respnose 类型,这个类型不是从 proto 继承而来,那么它就会说:”我不知道如何传递这个,所以我返回空值 null.』,然而,对于 GSON converter 而言,它通过返回一个实例来表明它可以处理这个类型的。这就是为什么这个类是一个工厂类,因为我们让它生产 converter 实例。

如果你想要做一些自定义,实现起来是非常容易的。Converter 的实现与之前的实现是非常相似的,尽管代替类型化的输入和输出,我们现在使用的是 OkHttp 的请求body 和相应 body。

interface Converter<T> {
  interface Factory {
	Converter <?> create(Type type);
  }
  T fromBody(ResponseBody body);
  RequestBody toBody(T value);
}

现在这已经高效的多,因为我们实际上可以查询那些 adapter。举个例子,GSON 有一个type adapter, 当我们请求 GSON Converter Factory,询问它是否可以处理某种请求的时候, converter factory 就开始查询这个 adapter, 它将以缓存的形式存在,当我们再次查询的时候,这个 adapter 就可以直接被使用了。这是一个非常小的成功,极力避免了不断地查询带来的损耗。

call adapter 有相同的模式。我们询问一个 call adapter factory 它是否可以处理某个类型,它将会以相同的方式回应。(例如:它会返回 null 来表达否)。它的API 是非常简单的。

interface CallAdapter<T> {
  interface Factory {
    CallAdapter<?> create(Type type);
  }
  Type responseType();
  Object adapt(Call<T> value);
}

我们有一个方法来来实现适配。传入一个 call 实例,返回了一个 observable,single 或者 future 等。 还有一种方法来获得这种 response 类型:当我们声明一系列 contributor 调用时, 我们没法自动把那些参数化的类型提取出来,因此我们基本上只是请求这个 call adapter 也返回这个 response type。如果你为这个变量创造了一个实例,我们会请求 call adapter, 它会将 contributor type 列表返回。

还在建设中 (40:05)

Retrofit 2 正在完善中!现在还不够完整,但是已经可以用了。我上面提到的点都是已经完成了的,那还有哪些未完成呢?

关于所谓的“参数 handler”我们现在还没有一个成熟的想法。我们未来想要让它有从 Guava 传递多个 map,或者数据类型及枚举类型。

日志功能还没有完成,在 Retrofit 1 里是有日志的,但是在 Retrofit 2 里面没有。依赖 OkHttp 的一个优点是你实际上可以使用一个 interceptor 来实现实际的底层的请求和响应日志。因此,对于原始请求和响应,我们并不需要它,但是我们很可能需要日志来记录 Java 类型。

如果你曾经使用过 mock 模块,你会发现它也还没被完成,但很快会完成的。

现在文档依然比较缺。

最后,在我有空的时候,我想在 Retrofit 2 里支持 WebSocket。在 2.0 里很可能无法实现,但是我想在后续的2.1 版本里会加入支持。

Release? (41:31)

我保证过 Retrofit 2 今年会和大家见面,今年确实可以。至于具体哪天问世,我们不会做任何承诺。我不想再在 2016 的 DroidCons 上开相同的玩笑。因此今年一定会问世。我保证。至于2015年8月27日,我已经开放了一个2.0的测试版。

dependencies {
  compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
  compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
  compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta1'
}

你可以依赖它。它已经可以正常工作了,API 接口也相对稳定。Converter 和 converter 工厂方法未来可能会改变,但是总体来说是有用的。要是你有什么不喜欢或者有问题的地方,请联系我!谢谢!

原文出处:https://realm.io/cn/news/droidcon-jake-wharton-simple-http-retrofit-2/