Retrofit使用指南

Retrofit is a type-safe HTTP client for Android and Java.

Retrofit是面向Android和Java平台的一个类型安全的HTTP客户端。

<br />
本文将围绕Retrofit的注解、CallAdapter(配合RxJava)、Converter进行介绍;

首先在你的工程添加以下依赖:

final OKHTTP_VERSION = '3.4.1'
final RETROFIT_VERSION = '2.1.0'
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"

版本号如有更新的可以更改到最新

接下来演示如何请求接口:
假设我们现在需要请求一个这样的接口: https://api.github.com/users/yuhengye
首先创建一个TestService接口

import retrofit2.Call;
import retrofit2.http.GET;

public interface TestService {

    //定义一条接口请求
    @GET("users/yuhengye")
    Call<String> getUserInfo();
}

先不关心为何创建一个这样的接口
接下来创建一个RetrofitManager.class

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitManager {

    private static Retrofit mRetrofit;

    private static TestService mTestService;

    private static OkHttpClient mOkHttpClient;

    public static OkHttpClient getOkHttpClient(){
        if(mOkHttpClient == null) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
            mOkHttpClient = new OkHttpClient.Builder()
                    .addInterceptor(logging)
                    .build();
        }
        return mOkHttpClient;
    }

    public static Retrofit getRetrofit(){
        if(mRetrofit == null) {
            mRetrofit = new Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(getOkHttpClient())
                    .build();

        }
        return mRetrofit;
    }

    public static TestService getTestService(){
        if(mTestService == null) {
            mTestService = getRetrofit().create(TestService.class);
        }
        return mTestService;
    }
}

接下来看下如何在代码里请求接口:

1.异步执行:

 Call<String> call = RetrofitManager.getTestService().getUserInfo();

 call.enqueue(new Callback<String>() {
     @Override
     public void onResponse(Call<String> call, Response<String> response) {

         //获得结果
         String result = response.body();

     }

     @Override
     public void onFailure(Call<String> call, Throwable t) {

     }
 });

2.同步执行

 Call<String> call = RetrofitManager.getTestService().getUserInfo();

 try {
     Response<String> response = call.execute();
     //获得结果
     String result = response.body();
 }catch (IOException ioException){
     ioException.printStackTrace();
 }

上面例子中的Call<String>里申明的call是用于操控网络请求的,而申明的泛型String类型这是我们想要获得请求结果时转换成的类型,下文会解释是怎么转换的;

在Android里不能在主线程执行请求网络的操作,所以使用同步请求是需要另起线程,所以一般都是异步请求;

如果你像以上所述操作后,那么你已经成功使用Retrofit请求网络。

接下来看下如何定义一条接口请求:

//定义一条接口请求
//等价于https://api.github.com/users/yuhengye
@GET("users/yuhengye")
Call<String> getUserInfo();

可以看到方法头部使用了注解,官方文档是这么解释的:

Every method must have an HTTP annotation that provides the request method and relative URL. There are five built-in annotations: GET, POST, PUT, DELETE, and HEAD. The relative URL of the resource is specified in the annotation.

<br />

每个方法都必须有一个HTTP注释提供的请求方法和相对URL。有五个内置注释:GET,POST,PUT,DELETE和HEAD。资源的相对URL在注释中指定。

<br />
也就是说如果你想用GET请求就可以在方法头部加@GET注释,使用POST请求就使用@POST即可,在注释里申明的相对URL会和在生成Retrofit实例时使用的baseUrl()方法里传的值结合;

Retrofit提供不同HTTP请求方法的注解:
GET,POST,PUT,DELETE,HEAD,OPTIONS,PATCH,HTTP;

Retrofit还提供了以下注解:
Path,Header,HeaderMap,Headers,Query,QueryMap,FormUrlEncoded,Field,FieldMap,Multipart,Part,PartMap,Body,Streaming,Url;

值得一提的是,Retrofit里提供的注解命名规范是这样的,如果是HTTP请求的方法,则一律是大写,其他注解才使用驼峰式;

接下来解释下这些注解如何使用,为了缩短篇幅,以下在调用接口时,就不写获取接口实例的方法了,比如调用getUserInfo()相当于RetrofitManager.getTestService().getUserInfo(),并且申明的接口方法都是在TestService接口下;


Path
@GET("users/{username}")
Call<String> getUserInfo(@Path("username") String name);

//等价于https://api.github.com/users/yuhengye
getUserInfo("yuhengye");

可以看出Path是用于替换相对路径url里的值;


Header,HeaderMap,Headers

//在方法里声明多个参数时,可以多种注解组合使用
@GET("users/{username}")
Call<String> getUserInfo(@Header("Accept-Language") String lang, @Path("username") String name);

//Request Header 
//Accept-Language:en-US
getUserInfo("en-US", "yuhengye");

---------------divider---------------    

@GET("users/yuhengye")
Call<String> getUserInfo(@Header("Accept-Language") List<String> langs);

List<String> langs = new ArrayList<>();
langs.add("en-US");
langs.add("zh-CN");

//Request Header 
//Accept-Language:en-US,zh-CN
getUserInfo(langs);

---------------divider---------------    

@GET("users/yuhengye")
Call<String> getUserInfo(@HeaderMap<String,String> headers);
     
Map<String,String> headers = new HashMap<>();
header.put("Accept", "text/plain");
header.put("Accept-Charset", "utf-8");
     
//Request Header 
//Accept:text/plain
//Accept-Charset:utf-8
getUserInfo(headers);

---------------divider---------------

@Headers("Accept-Language: en-US")
@GET("users/yuhengye")
Call<String> getUserInfo();

@Headers({
"Accept-Language: en-US,zh-CN",
"Cache-Control: max-age=640000"
})
@GET("users/yuhengye")
Call<String> getUserInfo2();

Header是用于给HTTP的请求头增加信息的,HeaderMap则是当有多个Header和数量不固定的Header准备的;下面介绍QueryQueryMapFieldFieldMap时也是一样的道理;而Headers是当你的接口的Header是固定的时候才用到,并且支持多个(数组形式);


Query,QueryMap

@GET("search/repositories")
Call<String> searchRepo(@Query("q") String keywords, @Query("sort") String sort); 

//等价于https://api.github.com/search/repositories?q=retrofit&sort=stars
searchRepo("retrofit", "stars");

---------------divider---------------

@GET("search/repositories?order=desc")
Call<String> searchRepoOrderByDesc(@QueryMap Map<String,String> params); 

Map<String,String> params = new HashMap<>();
header.put("q", "retrofit");

//等价于https://api.github.com/search/repositories?order=desc&q=retrofit
searchRepo(params);

QueryQueryMap主要是在GET请求时,给你的URL传参,当然URL的参数也可以固定在相对路径里面;


FormUrlEncoded,Field,FieldMap

@FormUrlEncoded
@POST(“https://api.weibo.com/oauth2/access_token”)
Call<String> oauthToken(@Field("access_token") String code, @Field("client_secret") String client_secret);

//POST表单数据
oauthToken("codeValue", "secretValue");

这里没有使用到FieldMap,相信你也知道它的用处是什么了,当你需要使用到表单的方式请求时,只需使用@FormUrlEncoded注解就可以了,实际上跟你替换成@Headers("Content-Type: application/x-www-form-urlencoded")同理,只是框架已经帮我们实现了,FieldFieldMapQueryQueryMap类似,只不过前者是在表单提交时使用的注解,后者是URL传参时使用的注解。

在上面例子中,@Post中的URL不再是相对路径,而是绝对路径,当你使用绝对路径时,Retrofit就会忽略掉你实例化它时通过baseUrl()方法传给它的host(接口前缀地址);因为实际开发中,有可能只有几个接口的host是跟你设置的host不一样的,为了避免实例化多个Retrofit,你只需传入绝对路径即可;


Body

@POST("users/new")
Call<String> createUser(@Body RequestBody user);

---------------divider---------------

@POST("users/new")
Call<String> createUser(@Body User user);

当发起POST请求时,使用@Body把参数直接放到你的request的body里面,如果在实例化Retrofit时,没有指定converter(下文会解释这是什么), 那@Body注解后面的参数类型必须是RequestBody类型;


Multipart,Part,PartMap

@Multipart
@POST(“https://api.weibo.com/2/statuses/upload_pic.json”)
Call<String> uploadPicture(
@Part("access_token") RequestBody token,
@Part MultipartBody.Part file);

RequestBody requestFile = RequestBody.create(MediaType.parse("application/otcet-stream"), new File(s));

MultipartBody.Part body = MultipartBody.Part.createFormData("pic", "share.png", requestFile);

uploadPicture(RequestBody.create(MediaType.parse("multipart/form-data"), "tokenValue"), body);

以下是POST请求时,multipart的body内容

//部分头信息
Content-Type: multipart/form-data; boundary=d1547544-7ffb-4ebd-913e-8cb21d2ea8f9
Content-Length: 32461

//body内容开始
--d1547544-7ffb-4ebd-913e-8cb21d2ea8f9
Content-Disposition: form-data; name="access_token"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 32
2.00tJ6X3GiGSdcC5f7433b5a40iAPJA
--d1547544-7ffb-4ebd-913e-8cb21d2ea8f9
Content-Disposition: form-data; name="pic"; filename="share.png"
Content-Type: application/otcet-stream
Content-Length: 31813
file bytes
--d1547544-7ffb-4ebd-913e-8cb21d2ea8f9--
//body内容结束

通常我们上传文件时,大多数使用multipart,下面简单描述下multipart的传输过程:
每一个部分都是以--加boundary(分隔符)开始,然后是该部分内容的描述信息,然后一个回车,然后是描述信息的具体内容;如果传送的内容是一个文件的话,那么还会包含文件名信息,以及文件内容的类型。上面请求的第二个部分就是是一个文件体的结构,最后会以--boundary符--结尾,表示请求体结束。

当使用@Multipart时,相当于@Headers("Content-Type: multipart/form-data; boundary=${bound}"),并且框架帮你把boundary(分隔符)的值设定好了;

@Part@PartMap是配合@Multipart使用的,使用@PartMap参数类型必须是Map,key就相当于partName, value所接受的参数类型跟@Part一样,但建议不要是MultipartBody.Part(因为MultipartBody.Part本身已经包含partName),以下是@Part接受不同参数时的情况:
1.当方法里传参类型是MultipartBody.Part时,不要这样(@Part("partName") MultipartBody.Part part申明,partName会被忽略,因为在构建MultipartBody.Part时,已经包含了part的name在里面;

2.当方法里传参类型是RequestBody时,会把RequestBody的ContentType的值加到body里面去,比如像上面例子这样申明Call<String> uploadPicture(@Part("access_token") RequestBody token);那么调用该方法uploadPicture(RequestBody.create(MediaType.parse("multipart/form-data"), "tokenValue"))时插入body里面的内容是这样的:

--d1547544-7ffb-4ebd-913e-8cb21d2ea8f9
Content-Disposition: form-data; name="access_token"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 32
2.00tJ6X3GiGSdcC5f7433b5a40iAPJA
--d1547544-7ffb-4ebd-913e-8cb21d2ea8f9--

3.当方法里传参类型是其他对象类型时,将会通过实例化Retrofit时指定的conveter(下文会解释这是什么)来转换成RequestBody,然后跟上面第二点所述处理;


Streaming

@Streaming
@GET("video/test.mp4")
Response getVideo();

Response response = getVideo(); 
InputStream videoDataStream = response.getBody().in();

使用@Streaming时返回值必须是Response,包含了HTTP请求最初的body内容,未经任何转换;一般是获取比较大的数据流时使用,或者下载文件时使用;


Url

//等价于https://api.github.com/users/yuhengye
@GET("users/yuhengye")
Call<String> getUserInfo();

---------------divider---------------    

@GET
Call<String> getUserInfo(@Url String url);

//等价于https://api.github.com/users/yuhengye
getUserInfo("users/yuhengye);

从上面可以看出,如果方法里申明带@Url的参数,则在方法头部申明HTTP请求方式时,可以不再申明相对路径地址;

实例化Retrofit时调用baseUrl(api)方法传进去的api地址应该以/结尾,如果传的是https://api.github.com/search,Retrofit会以/最后出现的位置作为结尾当成https://api.github.com/处理,值得一提的是,在申明请求接口头部定义url的时候,建议统一用相对路径(前面不包括/),类似@GET("users/yuhengye")而不是@GET("/users/yuhengye"),因为这两者处理上也有所不同:

BaseUrl:https://api.github.com/search/
Endpoint:/users/yuhengye
Result:https://api.github.com/users/yuhengye

BaseUrl:https://api.github.com/search/
Endpoint:users/yuhengye
Result:https://api.github.com/search/users/yuhengye


CallAdapter

上文在申明接口的方法时,返回值统一都是Call<String>类型,因为默认情况下返回值是只支持Call类型的,这个类包含了最基本的网络请求操作,相信你看过很多文章是XXX App,使用了以下框架:Retrofit + RxJava + ...,如果你没用过RxJava,可以跳过此段介绍,下面简单介绍RxJava结合Retrofit请求网络。

首先加入以下依赖:

compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"

//以下是下面例子中使用RxJava的版本   
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'

在实例化Retrofit时,改成这样:

public static Retrofit getRetrofit(){
  if(mRetrofit == null) {
      mRetrofit = new Retrofit.Builder()
              .baseUrl("https://api.github.com/")
              .addConverterFactory(GsonConverterFactory.create())
              .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
              .client(getOkHttpClient())
              .build();
    
  }
  return mRetrofit;
}

addCallAdapterFactory(RxJavaCallAdapterFactory.create())这个是我们增加的调用,以后在申明接口的方法返回值时,就可以这样:

@GET("users/yuhengye")
Observable<String> getUserInfo();

RetrofitManager
            .getTestService()
            .getUserInfo()//返回一个Observable<String>对象
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<String>() {
                @Override
                public void onCompleted() {
                }
                @Override
                public void onError(Throwable e) {
                }
                @Override
                public void onNext(String result) {
                }
            });

Converter(转换器)

上文中多次提到过converter的概念,现在简单介绍下:

Retrofit框架默认是使用OKHTTP去请求网络的,而在OKHTTP中网络请求默认都是返回一个ResponseBody类型的值,为了免去网络请求中重复的转换操作,在实例化Retrofit时可以通过addConverterFactory(GsonConverterFactory.create())方法增加一个转换工厂,Retrofit内置的转换工厂是只支持ResponseBodyVoid类型,值得一提的是,在接口申明的返回值里如果不关心返回结果,必须这样申明Call<Void>,不能没有泛型Call这样申明;

文章开始介绍要引入的依赖时,有包括这个compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION", 这个库的的名字是converter-gson,那么肯定是跟gson相关的了,回想一下,刚才我们申明接口方法时的返回值,统一都是Call<String>,之所以可以申明String类型,是因为我们通过这个依赖库提供的Gson转换工厂把结果转换成了String类型,然后请求返回结果后得到一个Response<String>类型的实例responseResponse申明的泛型类型是跟Call申明的泛型类型一致的,而String result = response.body()就是获得申明泛型类型的值,通常来说我们是知道请求接口会返回的数据格式,如果是json格式,每次都是通过获得String类型,再转换成对应的实体类,那么会做很多重复的工作;所以上文在接口申明方法时的泛型类型都可以替换成对应的实体类,比如这样:

//GitHubUser就是通过gson转换成对应的实体类;
@GET("users/yuhengye")
Call<GitHubUser> getUserInfo();

当然Converter还不止是在请求结果返回时转换结果,还包括发起请求时的一些数据转换等;以上所提到的@HeaderMap@QueryMapFieldMap后面申明的参数类型必须是Map,但是key和value没有限定必须是String类型,这3个注解和@Path@Header@Query@Field默认最终都会调用内置的converter调用String.valueOf(object)转成String类型,也就是调用Object的toString()方法;

我们也可以自定义自己的Converter工厂,本文就不再叙述了,后面会再写一遍关于Retrofit的源码和原理探索还有进阶使用;

相关文章:
Retrofit源码解析

参考文档:

Retrofit官方文档
http://square.github.io/retrofit/

HTTP协议之multipart/form-data请求分析
http://blog.csdn.net/five3/article/details/7181521/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容