Retrofit提供了广泛的功能,并且有许多可能的配置。 许多较大的应用程序需要一些特定的设置,例如OAuth身份验证。 为了实现一个干净和稳定的项目,我们将向您介绍我们对可持续Android客户端的想法:ServiceGenerator。本文是译文:原文地址

Retrofit 工作原理

Retrofit使用一个http客户端(OkHttp)来执行网络请求。这个执行会在后台线程进行。

当 OkHttp 客户端从服务器收到响应,其就把响应返回给Retrofit。Retrofit把这个响应 压到 converter内,然后把其包装在一个可用的有意义的Java对象内,这一步依然是在后台线程完成。最后,当所有的事情都干完后,Retrofit就会把结果返回到 app 的 UI线程。

默认情况下,返回的结果以 Call 形式进行封装。从一个接收和准备结果的线程返回到安卓的UI线程这个动作叫做 call adapter

ServiceGenerator

Retrofit对象及其构建器是所有请求的核心。在这里,您可以配置和准备请求,响应,身份验证,日志记录和错误处理。不幸的是,我们已经看到太多开发人员只是复制并粘贴这些部分而不是把这些操作分出来放在一个干净的类中。ServiceGenerator给出了解决办法,此是基于 Bart Kiers’ idea.

让我们从简单的代码开始吧。在其当前状态下,它仅定义一种方法来为给定的类/接口创建基本REST客户端,该方法从接口返回服务类。

public class ServiceGenerator {

private static final String BASE_URL = "https://api.github.com/";

private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());

private static Retrofit retrofit = builder.build();

private static OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();

public static <S> S createService(
Class<S> serviceClass) {
return retrofit.create(serviceClass);
}
}

createService方法接收一个serviceClass作为参数,这个serviceClass是一个用来进行API请求的接口,然后建立一个可用的客户端。在这个的客户端上,您将能够执行网络请求。

为什么ServiceGenerator都声明为static

只有一个原因,在整个App中,我们只想打开一个连接,使用同样的 Retrofit, OkHttpClient来控制所有的请求,响应,缓存等。只使用一个OkHttpClient来重用打开套接字是很普遍的做法。这就意味着,我们需要使用依赖注入来注入OkHttpClient到类中或使用一个静态字段。这里我们选择了静态字段。因为我们在整个类中使用OkHttpClient,所以我们需要使所有字段和方法都是静态的。

除了加快速度之外,当我们不必一次又一次地重新创建相同的对象时,我们可以在移动设备上节省一些宝贵的内存

使用 ServiceGenerator

我们来看一下建立一个请求的例子:

String API_BASE_URL = "https://api.github.com/";

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(
GsonConverterFactory.create()
);

Retrofit retrofit =
builder
.client(
httpClient.build()
)
.build();

GitHubClient client = retrofit.create(GitHubClient.class);

如果只是一个请求,这很简单。但是我们的应用一般都会调用N个接口,这就显得非常的麻烦了。使用 ServiceGenerator,事情就简单起来了:

GitHubClient client = ServiceGenerator.createService(GitHubClient.class);

一句代码就OK。

所有的事情都转移到了ServiceGenerator中。但多数时候,我们的ServiceGenerator不会这么简单,这就需要我们自己调整代码来适应我们的应用了。

日志

我们希望能看到Retrofit发出的或者收到的数据。Retrofit 2的日志通过一个叫 HttpLoggingInterceptor 拦截器完成,我们需要把这个拦截器的一个实例添加到OkHttpClient。可能会有如下这样的代码:

public class ServiceGenerator {

private static final String BASE_URL = "https://api.github.com/";

private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());

private static Retrofit retrofit = builder.build();

private static HttpLoggingInterceptor logging =
new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY);

private static OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();

public static <S> S createService(
Class<S> serviceClass) {
if (!httpClient.interceptors().contains(logging)) {
httpClient.addInterceptor(logging);
builder.client(httpClient.build());
retrofit = builder.build();
}

return retrofit.create(serviceClass);
}
}

在这里有几个地方要注意。首先不要多次添加拦截器,我们通过 httpClient.interceptors().contains(logging)来判断是否已添加。第二,不要每次都在createService方法中 构建 retrofit对象,这样我们使用 ServiceGenerator的目的就失败了。

认证准备

身份验证的要求略有不同。您可以在我们的基本身份验证,令牌身份验证,OAuth甚至Hawk身份验证教程中了解更多信息。因为每个身份验证实现的细节都有所不同,所以您可能需要更改ServiceGenerator。其中一个是您需要将额外参数传递给createService以创建客户端。

让我们看一下Hawk身份验证的示例:

public class ServiceGenerator {

private static final String BASE_URL = "https://api.github.com/";

private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());

private static Retrofit retrofit = builder.build();

private static HttpLoggingInterceptor logging =
new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY);

private static OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();

public static <S> S createService(
Class<S> serviceClass, final HawkCredentials credentials) {
if (credentials != null) {
HawkAuthenticationInterceptor interceptor =
new HawkAuthenticationInterceptor(credentials);

if (!httpClient.interceptors().contains(interceptor)) {
httpClient.addInterceptor(interceptor);

builder.client(httpClient.build());
retrofit = builder.build();
}
}

return retrofit.create(serviceClass);
}
}

我们的createService现在有了第二个参数HawkCredentials。如果传递非null值,它将创建必要的Hawk身份验证拦截器并将其添加到Retrofit客户端。我们还需要重建Retrofit以将我们的更改应用于接下来的请求。另外,您可能会在其他教程中看到ServiceGenerator略有不同的版本。别迷惑!我们建议您保持ServiceGenerator的纤薄并使用特定的使用场景。

错误处理

当我们在一个请求中要检测是否成功返回的时候:

call.enqueue(new Callback<List<GitHubRepo>>() {  
@Override
public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) {
if (response.isSuccessful()) {
Toast.makeText(ErrorHandlingActivity.this, "server returned so many repositories: " + response.body().size(), Toast.LENGTH_SHORT).show();
// todo display the data instead of just a toast
}
else {
// error case
switch (response.code()) {
case 404:
Toast.makeText(ErrorHandlingActivity.this, "not found", Toast.LENGTH_SHORT).show();
break;
case 500:
Toast.makeText(ErrorHandlingActivity.this, "server broken", Toast.LENGTH_SHORT).show();
break;
default:
Toast.makeText(ErrorHandlingActivity.this, "unknown error", Toast.LENGTH_SHORT).show();
break;
}
}
}
@Override
public void onFailure(Call<List<GitHubRepo>> call, Throwable t) {
Toast.makeText(ErrorHandlingActivity.this, "network failure :( inform the user and possibly retry", Toast.LENGTH_SHORT).show();
}
});

这样做没有错,但是非常的低效。我们必须在我们的每个请求中进行这样的处理操作,到处进行复制粘贴。而当我们要改变遇到这样情况时的行为,就会变成一个噩梦。即使我们将这段逻辑放到一个中心方法类,您也必须记住在每个响应回调中调用此方法。

处理全局错误场景的最佳方法是在所有请求的一个中心位置处理它们:一个OkHttp拦截器

全局错误处理: OkHttp Interceptor

关于拦截器

OkHttp Interceptors

拦截器是一种强大的机制,可以监视,重写和重试调用。下面这是一个简单的拦截器,记录传出请求和传入响应。

class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();

long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));

Response response = chain.proceed(request);

long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));

return response;
}
}

对chain.proceed(request)的调用是每个拦截器实现的关键部分。这种看起来很简单的方法是所有HTTP工作发生的地方,在这里产生满足请求的响应。

拦截器可以链式的。加入你有一个压缩拦截器和一个校验和拦截器,不可能会需要判断数据是否已被压缩,然后再进行校验和计算。OkHttp使用 List 来跟踪拦截器,拦截器按序调用。

应用拦截器

拦截器一般会注册成为 应用拦截器 或者 网络拦截器。我们用上面定义的 LoggingInterceptor来表现他们的不同之处。

OkHttpClient.Builder 上调用 addInterceptor 来注册成为应用拦截器。

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();

Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();

Response response = client.newCall(request).execute();
response.body().close();

http://www.publicobject.com/helloworld.txt 重定向到了 https://publicobject.com/helloworld.txt,OkHttp会自动跟随重定向。我们的应用拦截器调用一次,然后 从 chain.proceed() 返回的响应拥有 重定向的信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

我们可以看到我们被重定向了,因为response.request().url()request.url()并不相同。

网络拦截器

注册网络拦截器也很简单,只要调用addNetworkInterceptor()就OK了。

OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();

Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();

Response response = client.newCall(request).execute();
response.body().close();

运行的时候,拦截器会被调用两次。一次是在初始请求http://www.publicobject.com/helloworld.txt,另外一个是重定向后的请求https://publicobject.com/helloworld.txt

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

网络请求还包含更多数据,例如OkHttp添加的Accept-Encoding:gzip标头,用于通告支持响应压缩。网络拦截器的链具有非空Connection,可用于询问用于连接到Web服务器的IP地址和TLS配置。