Clean Architecture 学习之 Retrofit

前言

Retrofit 是 square 公司开源的一个非常著名的简化网络请求的框架,但是它不是网络框架,OkHttp 才是,Retrofit 相当于是将OkHttp封装了,方便我们使用。square公司还有很多其他非常著名的开源库,比如OkHttp,Otto 以及Dagger(Dagger2 是由google 维护的一个分支,专为移动设备开发的)。Bob大叔在Clean Architecture中使用了它,我们就来学习Retrofit的基本使用,学习完本文以后,你将能应付80%的日常开发工作。
最新版本的Retrofit 已经升级到2.1了,在2.0之前的版本,OkHttp是可选的,但是2.0之后是必须的了。2.0之前,Converter 是内置的,来自服务器的json字符串会自动转换为定义好的DAO(Data Access Object),2.0之后你必须自己手动引入。

添加依赖库

想要在项目中使用Retrofit非常简单,在build.gradle的依赖中加入下面这行代码:

compile 'com.squareup.retrofit2:retrofit:2.1.0'

添加converter依赖库

正如前面所说,如果Retrofit 2.X 不再内置converter,如果你想要接收json 并解析成DAO,你必须把Gson Converter作为一个独立的依赖添加进来:

compile 'com.squareup.retrofit:converter-gson:2.1.1'

Square还提供了很多其他常见的converter,比如Jackson、Protobuf 等到,参看详情请移步这个链接

添加自定义converter

如果你想使用自己定义的Converter,你必须继承 Converter.Factory 这个abstract类,覆盖其中的requestBodyConverterresponseBodyConverter方法。以FastJson 为例:

FastJsonConverterFactory.java

public class FastJsonConverterFactory extends Converter.Factory{
    private Charset charset;
    private static final Charset UTF_8  = Charset.forName("UTF-8");

    public static FastJsonConverterFactory create() {
        return create(UTF_8);
    }

    public static FastJsonConverterFactory create(Charset charset) {
        return new FastJsonConverterFactory(charset);
    }

    public FastJsonConverterFactory(Charset charset) {
        this.charset = charset;
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations,
                                                          Annotation[] methodAnnotations, Retrofit retrofit) {
        return new FastJsonRequestBodyConverter<>(type, charset);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        return new FastJsonResponseBodyConverter<>(type, charset);
    }
}

FastJsonRequestBodyConverter.java

public class FastJsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
    private Type type;
    private Charset charset;
    private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");

    public FastJsonRequestBodyConverter(Type type, Charset charset) {
        this.type = type;
        this.charset = charset;
    }

    @Override
    public RequestBody convert(T value) throws IOException {
        return RequestBody.create(MEDIA_TYPE, JSON.toJSONString(value).getBytes(charset));
    }
}

FastJsonResponseBodyConverter.java

public class FastJsonResponseBodyConverter <T> implements Converter<ResponseBody, T> {

    private Type type;
    private Charset charset;

    public FastJsonResponseBodyConverter() {
    }

    public FastJsonResponseBodyConverter(Type type, Charset charset) {
        this.type = type;
        this.charset = charset;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            return JSON.parseObject(value.string(), type);
        } finally {
            value.close();
        }
    }
}
添加CallAdapter依赖库

Retrofit 2.x允许你自定义CallAdapter来满足您的特殊需求。 官方推出了支持RxJava的CallAdapter,要想在Retrofit的编程中使用响应式编程,你必须加入以下依赖库:

"com.squareup.retrofit2:adapter-rxjava:2.1.0"

添加完以上这些依赖库之后,我们就可以在项目中使用了,先看看怎么创建Retrofit 实例:

Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(FastJsonConverterFactory.create())
                .baseUrl(BASE_URL)
                .build();

使用Retrofit

API 声明

按照Square官方的说明,我们需要按照一定的格式创建一个接口,定义我们将要使用的Client,比如下面这个REST github client:

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

所有的client必须用interface的方式来定义。方法必须用@GET @POST等Http 方法注解。需要动态设定的URL,必须在方法的注解中用大括号中包起来,并在方法的参数中设定。下面详细介绍其使用方法

请求方法

每个方法必须包含一个HTTP 注解,注解中提供了请求方法和相关的URL,Retrofit 内置了五个方法:GETPOSTPUTDELETEHEAD。对应的URL在注解中指定,如上面的例子所示。你还可以在URL中指定query参数:

@GET("users/list?sort=desc")

参数化控制URL

我们可以通过占位符和方法中的参数来动态地更新请求的URL。占位符是用大括号包起来的,响应的参数必须用@path注解,并且使用跟占位符相同的字符串。如上面的例子所示。请求参数也可以同时添加在方法的参数中。

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

对于比较复杂的query参数,我们还可以使用map:

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
请求体

我们可以使用@Body注解来指定一个对象作为http 请求体。

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

Retrofit 会使用我们在创建Retrofit实例时指定的converter,将这个对象转换。如果没有指定,则只能使用RequestBody。

Form encode 和 MultiPart

我们也可以声明方法为使用form-encode 和 multipart data。
如果我们在方法上使用了@FormUrlEncoded注解,retrofit就会发送form-encode data。每个键值对都必须用@Field注解,包含name,值则通过对象提供。

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

在方法上加上 @Multipart 注解来使用Multipart请求。Parts 则使用@Part注解来声明。

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

Multipart parts使用Retrofit的converter之一,或者可以通过实现 RequestBody 来处理序列化。

Header 控制

你可以通过 @Header 注解来设置静态的headers

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

你也可以 @Header 注解来动态更新。相应的参数必须提供给 @Header 注解。如果只为空,这个header就会被忽略。

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
URL定义方式

Retrofit 2.0使用了新的URL定义方式。Base URL与@Url 不是简单的组合在一起。

BASE_URL @URL RESULT
http://www.github.com/ list http://www.github.com/list
http://www.github.com/base/level/ list/ http://www.github.com/base/level/list/
http://www.github.com/base/level /list http://www.github.com/list

从上面的表格可以看出,如果@URL以/开头的,会覆盖BASE_URL中的部分。所以,以下规则我们最好遵从

  • BASE_URL: 总是以 /结尾
  • Url: 不要以 / 开头

同步和异步请求

在Retrofit 2.x 中,所有的请求都被封装成一个Call对象。在retrofit 2.X中,同步和异步请求的定义不再有区别。他们只在执行的时候不一样。
同步方法在主线程中执行,而安卓的主线程是UI线程,是不允许在主线程中执行网络请求的。所以,在安卓应用开发中,我们不使用同步请求。不过我们还是来简单了解下同步请求的执行方法。
client 定义如下:

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTasks();
}

同步请求:

TaskService taskService = ServiceGenerator.createService(TaskService.class);  
Call<List<Task>> call = taskService.getTasks();  
List<Task>> tasks = call.execute().body();  

通过执行Call对象的excute()方法就会调用同步请求,反序列化的response body 可通过 body()方法得到。

异步请求:

TaskService taskService = ServiceGenerator.createService(TaskService.class);  
Call<List<Task>> call = taskService.getTasks();  
call.enqueue(new Callback<List<Task>>() {  
    @Override
    public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
        if (response.isSuccessful()) {
            // tasks available
        } else {
            // error response, no access to resource?
        }
    }

    @Override
    public void onFailure(Call<List<Task>> call, Throwable t) {
        // something went completely south (like no internet connection)
        Log.d("Error", t.getMessage());
    }
}

实现CallBack类,并执行Call对象的enqueue()方法,我们就能执行异步请求。

我们前面也提到过,Retrofit 支持响应式编程。因为我们可以使用RxJava来写出优雅而又逻辑简单的代码:
client定义:

public interface GoodsService {
    @POST("categories.do")
    Observable<GoodsCategoriesResp> getGoodsCategories(@Body GoodsCategoriesReq req);
}

请求的返回值,不再是Call对象,而变成了Observable对象了。因为我们在创建Retrofit实例的时候,已经添加了CallAdapter了。

GoodsCategoriesReq req = new GoodsCategoriesReq();
req.setId(id);
Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(FastJsonConverterFactory.create())
                .baseUrl(BASE_URL)
                .build();
retrofit
    .create(GoodsService.class)
    .getGoodsCategories(req)
    .flatMap(categoriesResp->{
        if (categoriesResp.getCode() != 200){
            return Observable.error(new BusinessException(categoriesResp.getCode(),categoriesResp.getMsg()));
        }
        return Observable.just(categoriesResp);
    })
    .map(resp->{
        List<GoodsCategory> categories = new ArrayList<>();

        for(GoodsCategoriesResp.Category item: resp.getCategories()) {
            GoodsCategory category = new GoodsCategory();
            category.setId(item.getId());
            category.setParentId(id);
            category.setName(item.getName());
            category.setImg(item.getImg());
            category.setHasNext(item.getHasNext() == 1);

            categories.add(category);
        }
        return categories;
    })
    .subscribe(new BaseSubscriber<List<GoodsCategory>>() {
                    @Override
                    public void onCompleted() {

                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onNext(List<GoodsCategory> categories) {
                    }
        });

在上面的代码中,当Retrofit异步执行完getGoodsCategories(req)之后,我们就得到了一个Observable<GoodsCategoriesResp>对象,但这个对象并不能直接提供给上层UI使用,我们我们需要进行一次转换,将其转换成List<GoodsCategory>对象。如果有错误发生,Subscriber的onError方法就会被调用,我们在里面进行出错处理。我们用了一系列的链式操作,就完成了出错处理和数据转换等工作,逻辑简单明了。

上传和下载文件

上传文件

在Retrofit 2.x中,由于舍弃了1.x的TypedFile方式,所以只能借助OkHttp库来执行与网络有关的操作。在2.x中有两种方式可以选择,OkHttp的RequestBody或者MultipartBody.Part,把你的文件封装进request body中。先看看上传文件的接口定义

public interface FileUploadService {  
    @Multipart
    @POST("upload")
    Call<ResponseBody> upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);
}

@description只是包含在RequestBody实例中的一个字符串。而第二个参数才是真正的文件。使用@MultipartBody.Part类的原因是它允许我们发送文件的真正的名字,而不是request中的二进制文件的数据。
下面这段代码展示了在Android 客户端中上传文件,每个步骤都包含了注释。这个方法需要一个文件的URI。

private void uploadFile(Uri fileUri) {  
    // create upload service client
    FileUploadService service =
            ServiceGenerator.createService(FileUploadService.class);

    // https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
    // use the FileUtils to get the actual file by uri
    File file = FileUtils.getFile(this, fileUri);

    // create RequestBody instance from file
    RequestBody requestFile =
            RequestBody.create(MediaType.parse("multipart/form-data"), file);

    // MultipartBody.Part is used to send also the actual file name
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("picture", file.getName(), requestFile);

    // add another part within the multipart request
    String descriptionString = "hello, this is description speaking";
    RequestBody description =
            RequestBody.create(
                    MediaType.parse("multipart/form-data"), descriptionString);

    // finally, execute the request
    Call<ResponseBody> call = service.upload(description, body);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call,
                               Response<ResponseBody> response) {
            Log.v("Upload", "success");
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            Log.e("Upload error:", t.getMessage());
        }
    });
}

一个需要注意的地方是Content-Type。如果你拦截了OkHttp Client 并且将其修改为 application/json,服务器在反序列化你的文件的时候就会有问题。所以务必要确定Request Header 使用的是multipart/form-data,而不是application/json

下载文件

下载文件与其他普通的Request并没有区别,唯一需要注意的是返回值类型必须是ResponseBody类型,否则Retrofit就会去尝试解析和转换,无疑这会引起异常。

FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);

Call<ResponseBody> call = downloadService.downloadFileWithDynamicUrlSync(fileUrl);

call.enqueue(new Callback<ResponseBody>() {  
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        if (response.isSuccess()) {
            Log.d(TAG, "server contacted and has file");

            boolean writtenToDisk = writeResponseBodyToDisk(response.body());

            Log.d(TAG, "file download was a success? " + writtenToDisk);
        } else {
            Log.d(TAG, "server contact failed");
        }
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        Log.e(TAG, "error");
    }
});

此外,由于Retrofit默认在下载文件的时候是将其放在内存中的,所以在下载大文件的时候,我们需要:

  1. 在Client的方法上加上@Streaming注解。
  2. 将网络请求放在另一个后台线程中,否则Android会触发android.os.NetworkOnMainThreadException异常。

使用Interceptor

Interceptor即拦截器,意味着我们可以在进行网络请求的时候对这些请求进行拦截,对Request或者Response进行预先处理,最常见的莫过于模拟网络返回值了。

public class MockInterceptor implements Interceptor {

    private String responeJsonPath;

    @Inject
    public MockInterceptor() {
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        String responseString = createResponseBody(chain);

        return new Response.Builder()
                .code(200)
                .message(responseString)
                .request(chain.request())
                .protocol(Protocol.HTTP_1_0)
                .body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
                .addHeader("content-type", "application/json")
                .build();
    }

    private String createResponseBody(Chain chain) {

        String responseString = null;

        HttpUrl uri = chain.request().url();
        String path = uri.url().getPath();

        if (path.equals("login.do")) {
            responseString = UserJson.getContent();
        } else if (path.contains("logout.do")) {

        } else if (path.contains("register.do")) {

        }else if (path.contains("categories.do")) {
            responseString = CategoryJson.getContent();
        }
        return responseString;
    }

}

这里定义的拦截器是模拟网络请求的返回,所有的网络请求都将返回200,response body 则是文件解析得到的内容。
接下来,我们需要定义OkHttp Client,将我们定义的Interceptor设置进去:

public OkHttpClient provideOkHttpClient(MockInterceptor intercepter){

        return new OkHttpClient.Builder()
                .addInterceptor(intercepter)
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
}

最后就是把自定义的client设置到Retrofit中去了:

public Retrofit provideRetrofit(OkHttpClient client){
    return new Retrofit.Builder()
            .client(client)
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .addConverterFactory(FastJsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .build();
}

总结

本文只是对Retrofit的简单介绍,只涵盖了最基本的使用。更多的内容需要在以后的工作中遇到了再去学习了。更多教程请戳这个链接

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,846评论 25 707
  • Retrofit--相信大家都或多或少的听过和用过了,不知道是什么的证明你已经out了~我使用和研究Retrofi...
    zyyoona7阅读 3,575评论 9 36
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 简介 刚接触Retrofit的时候,就写了一篇简单的使用介绍:Retrofit 2.0基本使用方法,算是对Retr...
    Whyn阅读 2,839评论 4 24
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful2、Retrofit解析2...
    隔壁老李头阅读 3,979评论 8 19