Retrofit是什么

Retrofit是square公司出的一个网络框架,在最近的一个Android项目中,自己选用了Retrofit来实现与服务端的通信。为什么在诸多方案中选择了retrofit呢?retrofit提供的“type-safe”的特性是自己所看中的。

通过retrofit定义服务端API接口,代码如下,

public interface GitHubService {
  @GET("/users/{user}/repos")
  List<Repo> listRepos(@Path("user") String user);
}

服务端提供的API接口在上述代码中通过java annotation定义在interface中,外界可以调用listRepos方法来实现与服务器的交互。从这个例子中,我们可以知道使用retrofit有如下好处,

  • API接口定义是类型安全的
  • IDE可以进行有效的代码提示与编译检查
  • 定义API的开销很小,相当于定义一个接口函数,开发效率能得到有效提升

过去项目中的网络层都是自己手动封装的,满足了上面的第三点,前面两点没有满足。在第一次看到retrofit的时候,直接去实现了一个类似的接口封装,不过这次是选择了直接用,毕竟自己实现的没有比retrofit更好。在介绍完retrofit的用法之后,也会继续来看retrofit的实现细节,探讨下如何实现这样的框架。从过往经验来看,实现并不困难,想到以及设计出这样的方案才是难点所在。

Retrofit的基本操作

接口定义

在retrofit中如何定义接口已经在上面有过展示,这里再进行扩充下,代码示例来自Retrofit: A type-safe REST client for Android and Java

public interface GitHubService {
  // url参数通过@Path进行指定
  @GET("/users/{user}/repos")
  List<Repo> listRepos(@Path("user") String user);
  
  // query参数通过@Query指定
  @GET("/user")
  List<Repo> listRepos(@Query(user_id) String userId);
  
  // http body内容通过@Body指定
  @POST("/users/new")
  void createUser(@Body User user, Callback<User> cb);
  
  // form数据通过@Field指定
  @FormUrlEncoded
  @POST("/user/edit")
  User updateUser(@Field("first_name") String first, @Field("last_name") String last);
  
  // multipart通过@Multipart,@Part指定
  @Multipart
  @PUT("/user/photo")
  User updateUser(@Part("photo") TypedFile photo, @Part("description") TypedString description);
  
  // header通过@Headers指定
  @Headers("Cache-Control: max-age=640000")
  @GET("/widget/list")
  List<Widget> widgetList();
}

数据格式转换

retrofit定义了converter结构,默认支持通过gson进行json数据到java对象的转化,

Gson gson = new GsonBuilder()
    .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
    .registerTypeAdapter(Date.class, new DateTypeAdapter())
    .create();

RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .setConverter(new GsonConverter(gson))
    .build();

除了GsonConverter之外,还支持ProtoConverter、SimpleXMLConverter等用于处理protobuf、xml格式数据。也可以通过自定义converter来实现特有的格式转化。以个人经验来看,选用protobuf对客户端是最为方便的,连实现java对象类都省了,但如果不能够做到服务端也同意这个格式的话,那就选用json。

请求拦截处理

retrofit也定义了RequestInterceptor结构,支持拦截每一个http请求进行处理,比如下面例子对每个请求都加上指定的header,

RequestInterceptor requestInterceptor = new RequestInterceptor() {
  @Override
  public void intercept(RequestFacade request) {
    request.addHeader("User-Agent", "Retrofit-Sample-App");
  }
};

RestAdapter restAdapter = new RestAdapter.Builder()
  .setEndpoint("https://api.github.com")
  .setRequestInterceptor(requestInterceptor)
  .build();

通过RequestInterceptor可以进行一些一致性的全局处理。

缓存处理

在retrofit的文档里没有找到如何使用缓存的直接描述,不过retrofit可以通过配合okhttp进行http结果缓存,

File httpCacheDirectory = new File(application.getApplicationContext()
    .getCacheDir().getAbsolutePath(), "HttpCache");

HttpResponseCache httpResponseCache = null;
try {
   httpResponseCache = new HttpResponseCache(httpCacheDirectory, 10 * 1024);
} catch (IOException e) {
   Log.e(getClass().getSimpleName(), "Could not create http cache", e);
}

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setResponseCache(httpResponseCache);
builder.setClient(new OkClient(okHttpClient));

缓存会对http get操作生效,post操作不会被缓存。

调用方式

调用retrofit定义的接口有三种方式:synchrous,asynchrous,observable。

前两种方式是大家都很熟悉的同步与异步方式。在Android当中,网络请求需要在非UI线程调用,因此直接用异步调用方式即可。

一个接口采用何种方式调用,retrofit是直接通过接口定义来判断的,

如下,接口定义直接返回具体类型的,为同步调用方式,

@GET("/user/{id}/photo")
Photo getUserPhoto(@Path("id") int id);

接口参数中定义了Callback的,则用异步方式进行调用,

@GET("/user/{id}/photo")
void getUserPhoto(@Path("id") int id, Callback<Photo> cb);

observable方式的定义,则是接口返回observable类型,

@GET("/user/{id}/photo")
Observable<Photo> getUserPhoto(@Path("id") int id);

observable是一个新的概念,之前也没怎么接触过。在目前的实际开发中还没有使用过这种方式,不过下面也来探讨下,这个方式适用在何种场景下。没有适用场景的话,retrofit也没必要集成进来。

将调用方法隐藏在接口定义中,也是一个比较精巧的设计。

多请求处理策略

在stackoverflow看了retrofit下使用observable的相关讨论,对于单一API请求来说,使用observable并没有太大优势,这里指的是实现代码的便利性上。使用基于rxjava的observable模式只是使得请求API调用处理变得更加函数式,对于曾经搞过scala、以及现在主要还在用python的自己来说,函数式特性并没有那么大的吸引力。

在相关讨论中,使用observable最大的好处其实是对多API并行处理进行了封装于简化,直接摘录stackoverflow上提供的一个代码,

// Callback

api.getUserDetails(userId, new Callback<UserDetails>() {
    @Override
    public void onSuccess(UserDetails details, Response response) {
        this.details = details;
        if(this.photos != null) {
            displayPage();
        }
    }
});

api.getUserPhotos(userId, new Callback<List<Photo>>() {
    @Override
    public void onSuccess(List<Photo> photos, Response response) {
        this.photos = photos;
        if(this.details != null) {
            displayPage();
        }
    }
});

// RxJava

private class Combined {
    UserDetails details;
    List<Photo> photos;
}


Observable.zip(api.getUserDetails(userId), api.getUserPhotos(userId), new Func2<UserDetails, List<Photo>, Combined>() {
            @Override
            public Combined call(UserDetails details, List<Photo> photos) {
                Combined r = new Combined();
                r.details = details;
                r.photos = photos;
                return r;
            }
        }).subscribe(new Action1<Combined>() {
            @Override
            public void call(Combined combined) {
            }
        });

从上述代码中可以只观对比出在多请求并行处理上两种方式的差异。

总结

移动客户端代码的相当一部分就是在处理客户端服务端交互逻辑,所以网络框架的选择就很重要。仅从功能上来看,任何一个框架都能胜任。性能上各个框架的差距也没有大到可以明显感知的地步。因此个人觉得选择网络框架的一个重要考量在于是否能够极大提升开发效率。在目前个人已知的选择中,retrofit的设计是最能满足这一需求的。

参考