Retrofit 2使用方法举例

  RetrofitSquare推出的一个“type-safe HTTP client for Android and Java”,通过将HTTP(REST) API转换为Java接口,极大地简化了HTTP API的使用。下面通过一个例子介绍Retrofit的基本使用,通过Retrofit向TMDb请求流行电影的信息,得到JSON格式的响应,由GSON解析后,以列表的形式显示出来。

0. The Movie Database

  下面的例子中使用了The Movie Database(TMDb)的API来获取当前流行电影的信息。要使用TMDb的API,首先要获取一个API key,注册TMDb后,在这里申请API Key,然后点击账号名称,在左边栏的“API” / “Details”下查看自己的API key,如图1所示。

图1

图1

 

  下面例子中使用“/discover/movie”端点来获取一系列电影的信息,如:

http://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=[YOUR API KEY]

可以得到以流行度(popularity)降序(desc)排列的一系列电影,注意把[YOUR API KEY] 替换为你自己的API key。将请求粘贴到浏览器,成功的话会得到一堆挤在一起的JSON文本,为了方便阅读,可以使用一些工具来将它格式化一下,格式化后结果可以参考这里

1. 获取Retrofit

  新建工程,在/app/build.gradle中dependencies下加入retrofit:

dependencies {
   ...
   compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
}

然后执行“Tools” / “Android” / “Sync Project with Gradle Files”,Retrofit就会被下载并添加到工程中。

  不要忘了在AndroidManifest.xml中添加INTERNET权限:

<uses-permission android:name="android.permission.INTERNET" />

2.创建POJO

  POJO即Plain Old Java Object,它类似于JavaBean,但可以不遵守JavaBean的一些约定。我们从服务器得到JSON格式的响应,使用GSON反序列化到POJO中。

  首先来看一下TMDb返回的JSON,截取开头部分如下:

{  
   "page":1,
   "results":[  
      {  
         "poster_path":"\/fYzpM9GmpBlIC893fNjoWCwE24H.jpg",
         "adult":false,
         "overview":"Thirty years after defeating the Galactic Empire, Han Solo and his allies face a new threat from the evil Kylo Ren and his army of Stormtroopers.",
         ...
      },
      {  
         "poster_path":"\/D6e8RJf2qUstnfkTslTXNTUAlT.jpg",
         "adult":false,
         "overview":"Armed with the astonishing ability to shrink in scale but increase in strength, con-man Scott Lang must embrace his inner-hero and help his mentor, Dr. Hank Pym, protect the secret behind his spectacular Ant-Man suit from a new generation of towering threats. Against seemingly insurmountable obstacles, Pym and Lang must plan and pull off a heist that will save the world.",
         ...
      },
      ...

“page”:1 说明当前位于第一页,“results” 后是一个长度为20的列表,列表中的每个元素描述了一部电影的信息,包括“poster_path” 、“adult” 、“overview” 等。我们需要创建两个POJO类,一个对应包括“poster_path” 、“adult” 、“overview” 等的电影信息本身Movie类,一个对应包括“page” 和“results” 的整个响应MovieResponse类。

2.1. 创建Movie

  根据JSON中对电影的描述,创建Movie类,Movie类中包含若干成员变量,与JSON中描述电影的字段一一对应。比如对应“adult” 和“overview” ,定义:

private boolean adult;
private String overview;

注意这里的成员变量名要和JSON中字段的名称一致(之后要使用Gson对JSON进行格式化)。

  当然成员变量也可以使用不同的名称,如对应“poster_path” ,定义:

@SerializedName("poster_path")
private String posterPath;

这里成员变量名称为posterPath,通过@SerializedName(“poster_path”),告诉GSON它对应了”poster_path”字段。使用@SerializedName 需要在gradle中dependencies下添加:
compile 'com.google.code.gson:gson:2.5'

并在Movie.java中导入:
import com.google.gson.annotations.SerializedName;

  完整文件可以参考这里

2.2. 创建MovieResponse

  对应JSON中的页数和电影列表,创建MovieResponse类,MovieResponse中包含“page” 和“results” ,“results” 是一个Movie列表:

public class MovieResponse {
    private long page;

    @SerializedName("results")
    private List<Movie> movies;

    public List<Movie> getMovies() {
        return movies;
    }
}

  完整文件可以参考这里

3. 定义接口

  接下来根据TMDb的API,使用Retrofit提供的annotation定义对应的接口。如前所述,这里使用了TMDb提供的“/discover/movie”端点来获取一系列电影的信息,参考文档,我们只使用sort_by 一个参数,如前面给出的:

http://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=[YOUR API KEY]

  据此定义接口:
public interface MovieService {
    String SORT_BY_POPULARITY_DESC = "popularity.desc";
    String SORT_BY_VOTE_AVERAGE_DESC = "vote_average.desc";
    String SORT_BY_VOTE_COUNT_DESC = "vote_count.desc";

    @GET("/3/discover/movie")
    Call<MovieResponse> getMovies(@Query("sort_by") String sortBy);
}

@GET 表示这是一个GET方法,“/3/discover/movie” 指明请求的端点,Call<MovieResponse> 表示返回MovieResponse,@Query(“sort_by”) String sortBy 表示在“/3/discover/movie” 后面追加“sort_by” 请求参数,参数的值由String sortBy 指定。

  完整文件可以参考这里

4. 创建Retrofit

4.1. 添加Gson

  默认情况下,Retrofit将HTTP正文反序列化为OkHttp的ResponseBody,可以通过添加额外的转换器来获得对其他类型的支持。

  在我们的例子中,需要将JSON反序列化为之前定义的MovieResponse,这里使用Gson来解析JSON。在gradle中dependencies下添加(在2.1.中已添加过):

compile 'com.google.code.gson:gson:2.5'

然后创建Gson:
Gson gson = new GsonBuilder().create();

4.2. 添加OkHttpClient

  在不同的平台上,Retrofit使用不同的HTTP client。在JVM上, Retrofit默认使用HttpUrlConnection,在Android上, 默认使用Apache HttpClient(Android 2.2及之前)或HttpUrlConnection HttpClient(Android 2.3及之后),也可以指定使用其他的client。

  之前在定义接口getMovies()时,并没有加入与API key对应的参数,因为API key对于每个请求都是必须的,希望通过HTTP client在请求的末尾追加API key,以简化调用。这里使用OkHttp实现这一功能。首先在gradle中dependencies下添加:

compile 'com.squareup.okhttp:okhttp:2.7.0'

然后创建OkHttpClient:
OkHttpClient httpClient = new OkHttpClient();

4.2.1. 追加API key参数

  为OkHttpClient添加Interceptor:

httpClient.interceptors().add(new Interceptor() {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request original = chain.request();
        HttpUrl originalHttpUrl = original.httpUrl();

        HttpUrl.Builder builder = originalHttpUrl.newBuilder()
                .addQueryParameter("api_key", BuildConfig.API_KEY);

        Request.Builder requestBuilder = original.newBuilder()
                .url(builder.build())
                .method(original.method(), original.body());

        Request request = requestBuilder.build();
        return chain.proceed(request);
    }
});

其中addQueryParameter(“api_key”, BuildConfig.API_KEY) 为查询请求追加“api_key” 参数,参数的值为BuildConfig.API_KEY 。这里使用了BuildConfig来获取API key,详见这里

4.2.2. 打开Log

  为了方便调试,可以打开Log:

httpClient.interceptors().add(
        new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));

4.3. 创建Retrofit

  最后来创建Retrofit:

final String BASE_URL = "http://api.themoviedb.org";
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(httpClient)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();

这里还指定了BASE_URL ,前面定义getMovies()时使用的端点”/3/discover/movie”就是以此为基础。

  得到Retrofit对象后,进一步创建接口:

movieService = retrofit.create(MovieService.class);

movieService 是MovieService类型的成员变量,其他类可以使用get方法获取MovieService,并调用其中的方法。
private MovieService movieService;
...
public MovieService getMovieService() { return movieService; }

  完整文件可以参考这里

5. 调用接口

  调用接口时,首先获取MovieService:

MovieService movieService = App.getRestClient().getMovieService();

然后调用getMovies(),得到Call<MovieResponse>:
Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC);

  到这里为止,Retrofit还没有把查询请求发给服务器,Retrofit提供同步和异步的方法来进行请求。使用同步的方法只需调用call.execute();即可,向服务器的请求一般耗时较长,为防止卡顿,在UI线程应使用异步的方法:
call.enqueue(new Callback<MovieResponse>() {
    @Override
    public void onResponse(Response<MovieResponse> response, Retrofit retrofit) {
        if (response.isSuccess()) {
            MovieResponse movieResponse = response.body();
            mMovies = movieResponse.getMovies();
            Log.v(LOG_TAG, "onResponse(): mMovies size = " + mMovies.size());

            updateListView();
        } else {
            int statusCode = response.code();
            Toast.makeText(MainActivity.this,
                    "Error code: " + statusCode, Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    public void onFailure(Throwable t) {
        Toast.makeText(MainActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
    }
});

其中call.enqueue() 使用回调函数的方法实现异步的请求,请求完成后,onResponse()会被调用,通过response.body() 获取由Gson反序列化后得到的movieResponse,并进一步通过movieResponse.getMovies() 得到Movie列表,之后更新UI。

  需要注意的是,只要请求有回应,即便请求失败,onResponse()都会被调用,上面的代码使用isSuccess() 来确认请求是否成功(返回2XX)。

  这里也是Retrofit 1.x版本和2.0版本之间的一个显著差异,1.x版本对应同步和异步的请求,需要定义不同的接口,在接口内定义回调函数;而2.0版本无论同步还是异步,接口定义都是一样的,只在调用方法上进行区别。如果发生连接错误,如没有网络连接,则会调用onFailure()。

  完整文件可以参考这里

6. 完整代码

  完整代码可以在这里找到,使用了一个ListView来显示电影名称、发行时间和平均评分,运行效果如图2所示。

图2

图2

7. 更多

  上面的例子只展示了Retrofit的一小部分功能,更多内容还请参考官方文档Consuming APIs with Retrofit这篇文章也十分有用。值得一提的是,Retrofit还支持RxJava,可以使用Observable来代替Call,如:

@GET("/3/discover/movie")
Observable<MovieResponse> getMovies(@Query("sort_by") String sortBy);