×

Android 手把手教你使用Retrofit2

96
wo叫天然呆
2017.01.12 17:03* 字数 945

Android 手把手教你使用Retrofit2

本文原创,转载请注明出处。欢迎关注我的 简书

前言:

1.我想通过本文,让大家能更快的上手Retrofit2,跟上时代的步伐。
2.本文主要是写给初级、中级开发人员阅读,所以在这边不会做太多的解释说明,只针对一些坑以及使用方法进行说明。
3.本文只涉及到Retrofit2,并未与RxJava一起使用,RxJava+Retrofit2技术将会在下篇文章中呈现。
4.本人也是刚开始接触Retrofit2,所以可能文章中会有一些错误或者不足,希望大家多多留言探讨,共同进步

介绍个框架给大家:

Novate https://github.com/Tamicer/Novate
感兴趣的朋友可以用用,还不错,相关资料在
Novate:Retrofit2.0和RxJava的又一次完美改进加强!(九)
大家也可以关注他:@Tamic,里面的文章都还不错,至少排版上比我的好多了,哈哈

开启Retrofit2之旅

添加依赖

以下是我用到的一些依赖

    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    //日志拦截器
    compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
    compile 'io.reactivex:rxjava:1.2.4'
    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'org.ligboy.retrofit2:converter-fastjson-android:2.1.0'

注解

retrofit通过使用注解来简化请求,大体分为以下几类:
1.用于标注请求方式的注解
2.用于标记请求头的注解
3.用于标记请求参数的注解
4.用于标记请求和响应格式的注解

请求方法注解

注解 说明
@GET get请求
@POST post请求
@PUT put请求
@DELETE delete请求
@PATCH patch请求,该请求是对put请求的补充,用于更新局部资源
@HEAD head请求
@OPTIONS option请求
@HTTP 通用注解,可以替换以上所有的注解,其拥有三个属性:method,path,hasBody

请求头注解

注解 说明
@Headers 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在
@Header 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头

请求参数注解

名称 说明
@Body 多用于post请求发送非表单数据,比如想要以post方式传递json格式数据
@Filed 多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用
@FiledMap 和@Filed作用一致,用于不确定表单参数
@Part 用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况
@PartMap 用于表单字段,默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传
@Path 用于url中的占位符
@Query 用于Get中指定参数
@QueryMap 和Query使用类似
@Url 指定请求路径

请求和响应格式注解

名称 说明
@FormUrlEncoded 表示请求发送编码表单数据,每个键值对需要使用@Field注解
@Multipart 表示请求发送multipart数据,需要配合使用@Part
@Streaming 表示响应用字节流的形式返回.如果没使用该注解,默认会把数据全部载入到内存中.该注解在在下载大文件的特别有用

使用

这里不对注解进行过多的说明了,只展示几个用法

/**
 * Created by caihan on 2017/1/11.
 *
 * @GET 表明这是get请求
 * @POST 表明这是post请求
 * @PUT 表明这是put请求
 * @DELETE 表明这是delete请求
 * @PATCH 表明这是一个patch请求,该请求是对put请求的补充,用于更新局部资源
 * @HEAD 表明这是一个head请求
 * @OPTIONS 表明这是一个option请求
 * @HTTP 通用注解, 可以替换以上所有的注解,其拥有三个属性:method,path,hasBody
 * @Headers 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在
 * @Header 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头
 * @Body 多用于post请求发送非表单数据, 比如想要以post方式传递json格式数据
 * @Filed 多用于post请求中表单字段, Filed和FieldMap需要FormUrlEncoded结合使用
 * @FiledMap 和@Filed作用一致,用于不确定表单参数
 * @Part 用于表单字段, Part和PartMap与Multipart注解结合使用, 适合文件上传的情况
 * @PartMap 用于表单字段, 默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传
 * <p>
 * Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。
 * 而Filed则相对简单些,通常是字符串键值对。
 * </p>
 * Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。
 * 而Filed则相对简单些,通常是字符串键值对。
 * @Path 用于url中的占位符,{占位符}和PATH只用在URL的path部分,url中的参数使用Query和QueryMap代替,保证接口定义的简洁
 * @Query 用于Get中指定参数
 * @QueryMap 和Query使用类似
 * @Url 指定请求路径
 */
public interface MyRetrofit2Service {

    //使用@Headers添加多个请求头
    @Headers({
            "User-Agent:android",
            "apikey:123456789",
    })
    @POST()
    Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);

    @GET("mobile/active")
    Call<HttpResult<News>> get(@Header("token") String token, @Query("id") int activeId);

    @GET("active/list")
    Call<HttpResult<News>> ActiveList();

    @POST("api/news/newsList")
    Call<HttpResult<News>> post2(@QueryMap Map<String, String> map);

    /**
     * 很多情况下,我们需要上传json格式的数据。比如当我们注册新用户的时候,因为用户注册时的数据相对较多,
     * 并可能以后会变化,这时候,服务端可能要求我们上传json格式的数据。此时就要@Body注解来实现。
     * 直接传入实体,它会自行转化成Json
     *
     * @param url
     * @param post
     * @return
     */
    @POST("api/{url}/newsList")
    Call<HttpResult<News>> login(@Path("url") String url, @Body News post);

    /**
     * 单张图片上传
     * retrofit 2.0的上传和以前略有不同,需要借助@Multipart注解、@Part和MultipartBody实现。
     *
     * @param url
     * @param file
     * @return
     */
    @Multipart
    @POST("{url}")
    Call<HttpResult<News>> upload(@Path("url") String url, @Part MultipartBody.Part file);

    /**
     * 多张图片上传
     *
     * @param map
     * @return
     */
    @Multipart
    @POST("upload/upload")
    Call<HttpResult<News>> upload(@PartMap Map<String, MultipartBody.Part> map);

    /**
     * 图文混传
     *
     * @param post
     * @param map
     * @return
     */
    @Multipart
    @POST("")
    Call<HttpResult<News>> register(@Body News post, @PartMap Map<String, MultipartBody.Part> map);

    /**
     * 文件下载
     *
     * @param fileUrl
     * @return
     */
    @Streaming
    @GET
    Call<HttpResult<News>> downloadPicture(@Url String fileUrl);

    /**
     * 这里需要注意的是如果下载的文件较大,比如在10m以上,那么强烈建议你使用@Streaming进行注解,否则将会出现IO异常.
     *
     * @param fileUrl
     * @return
     */
    @Streaming
    @GET
    Observable<HttpResult<News>> downloadPicture2(@Url String fileUrl);

    @POST()
    @FormUrlEncoded
    Observable<HttpResult<News>> executePost(@FieldMap Map<String, Object> maps);
}

踩坑

1.url被转义
http://api.mydemo.com/api%2Fnews%2FnewsList?

罪魁祸首@Url与@Path注解,我们开发过程中,肯定会需要动态的修改请求地址
两种动态修改方式如下:

    @POST()
    Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);

    @POST("api/{url}/newsList")
    Call<HttpResult<News>> login(@Path("url") String url, @Body News post);

第一种是直接使用@Url,它相当于直接替换了@POST()里面的请求地址
第二种是使用@Path("url"),它只替换了@POST("api/{url}/newsList")中的{url}
如果你用下面这样写的话,就会出现url被转义

    @POST("{url}")
    Call<HttpResult<News>> post(@Path("url") String url);

你如果执意要用@Path,也不是不可以,需要这样写

    @POST("{url}")
    Call<HttpResult<News>> post(@Path(value = "url", encoded = true) String url);

OkHttpClient

拦截器

addNetworkInterceptor添加的是网络拦截器Network Interfacetor它会在request和response时分别被调用一次;
addInterceptor添加的是应用拦截器Application Interceptor他只会在response被调用一次。

1.日志拦截器

使用addNetworkInterceptor()方法添加到OkHttpClient
日志拦截器我这有两种创建方式:
一种是使用HttpLoggingInterceptor,需要使用到依赖

compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
    /**
     * 创建日志拦截器
     *
     * @return
     */
    public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(
                new HttpLoggingInterceptor.Logger() {

                    @Override
                    public void log(String message) {
                        Log.e("OkHttp", "log = " + message);
                    }

                });
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        return loggingInterceptor;
    }

另一种是

    /**
     * 创建日志拦截器
     *
     * @return
     */
    private class LogInterceptor implements Interceptor {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            long t1 = System.nanoTime();
            Log.d("OkHttp", "HttpHelper1" + String.format("Sending request %s on %s%n%s",
                    request.url(), chain.connection(), request.headers()));

            okhttp3.Response response = chain.proceed(request);
            long t2 = System.nanoTime();

            Log.d("OkHttp", "HttpHelper2" + String.format("Received response for %s in %.1fms%n%s",
                    response.request().url(), (t2 - t1) / 1e6d, response.headers()));
            return response;
        }
    }
2.请求头拦截器

使用addInterceptor()方法添加到OkHttpClient
我的理解是,请求头拦截器是为了让服务端能更好的识别该请求,服务器那边通过请求头判断该请求是否为有效请求等...

    /**
     * 拦截器Interceptors
     * 使用addHeader()不会覆盖之前设置的header,若使用header()则会覆盖之前的header
     *
     * @return
     */
    public static Interceptor getRequestHeader() {
        Interceptor headerInterceptor = new Interceptor() {

            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request originalRequest = chain.request();
                Request.Builder builder = originalRequest.newBuilder();
                builder.addHeader("version", "1");
                builder.addHeader("time", System.currentTimeMillis() + "");

                Request.Builder requestBuilder = builder.method(originalRequest.method(), originalRequest.body());
                Request request = requestBuilder.build();
                return chain.proceed(request);
            }

        };
        return headerInterceptor;
    }
3.统一请求拦截器

使用addInterceptor()方法添加到OkHttpClient
统一请求拦截器的功能跟请求头拦截器相类似

    /**
     * 拦截器Interceptors
     * 统一的请求参数
     */
    private Interceptor commonParamsInterceptor() {
        Interceptor commonParams = new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request originRequest = chain.request();
                Request request;
                HttpUrl httpUrl = originRequest.url().newBuilder().
                        addQueryParameter("paltform", "android").
                        addQueryParameter("version", "1.0.0").build();
                request = originRequest.newBuilder().url(httpUrl).build();
                return chain.proceed(request);
            }
        };

        return commonParams;
    }
4.时间拦截器

使用addInterceptor()方法添加到OkHttpClient
从响应中获取服务器返回的时间

    /**
     * 拦截器Interceptors
     * 通过响应拦截器实现了从响应中获取服务器返回的time
     *
     * @return
     */
    public static Interceptor getResponseHeader() {
        Interceptor interceptor = new Interceptor() {

            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                okhttp3.Response response = chain.proceed(chain.request());
                String timestamp = response.header("time");
                if (timestamp != null) {
                    //获取到响应header中的time
                }
                return response;
            }
        };
        return interceptor;
    }

缓存

使用okhttp缓存的话,先要创建Cache,然后在创建缓存拦截器

        //创建Cache
        File httpCacheDirectory = new File(MyApp.getContext().getCacheDir(), "OkHttpCache");
        Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
        httpClientBuilder.cache(cache);
        //设置缓存
        httpClientBuilder.addNetworkInterceptor(getCacheInterceptor2());
        httpClientBuilder.addInterceptor(getCacheInterceptor2());
缓存拦截器

缓存时间自己根据情况设定

    /**
     * 在无网络的情况下读取缓存,有网络的情况下根据缓存的过期时间重新请求,
     *
     * @return
     */
    public Interceptor getCacheInterceptor2() {
        Interceptor commonParams = new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                if (!NetworkUtils.isConnected()) {
                    //无网络下强制使用缓存,无论缓存是否过期,此时该请求实际上不会被发送出去。
                    request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                okhttp3.Response response = chain.proceed(request);
                if (NetworkUtils.isConnected()) {//有网络情况下,根据请求接口的设置,配置缓存。
                    //这样在下次请求时,根据缓存决定是否真正发出请求。
                    String cacheControl = request.cacheControl().toString();
                    //当然如果你想在有网络的情况下都直接走网络,那么只需要
                    //将其超时时间这是为0即可:String cacheControl="Cache-Control:public,max-age=0"
                    int maxAge = 60 * 60; // read from cache for 1 minute
                    return response.newBuilder()
//                            .header("Cache-Control", cacheControl)
                            .header("Cache-Control", "public, max-age=" + maxAge)
                            .removeHeader("Pragma")
                            .build();
                } else {
                    //无网络
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    return response.newBuilder()
//                            .header("Cache-Control", "public,only-if-cached,max-stale=360000")
                            .header("Cache-Control", "public,only-if-cached,max-stale=" + maxStale)
                            .removeHeader("Pragma")
                            .build();
                }

            }
        };
        return commonParams;
    }

自定义CookieJar

        httpClientBuilder.cookieJar(new CookieJar() {
            final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();

            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                cookieStore.put(url, cookies);//保存cookie
                //也可以使用SP保存
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                List<Cookie> cookies = cookieStore.get(url);//取出cookie
                return cookies != null ? cookies : new ArrayList<Cookie>();
            }
        });

启动Retrofit2

到了这里,基本上准备工作都做好了,可以启动Retrofit2了

        retrofit = new Retrofit.Builder()
                .client(httpClientBuilder.build())
                .baseUrl(BASE_URL)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(FastJsonConverterFactory.create())
                .build();
        mApi = retrofit.create(MyRetrofit2Service.class);

界面上通过getEnqueue()方法调用

    /**
     * 异步调用
     */
    public void getEnqueue() {
        Call<HttpResult<News>> call = mApi.post(NEWS_URI, params);
        call.enqueue(new Callback<HttpResult<News>>() {
            @Override
            public void onResponse(Call<HttpResult<News>> call, Response<HttpResult<News>> response) {
                //处理请求成功
                Log.e("OkHttp", "处理成功请求 response = " + response.body().toString());
            }

            @Override
            public void onFailure(Call<HttpResult<News>> call, Throwable t) {
                //处理请求失败
                Log.e("OkHttp", "处理失败请求");
            }
        });
//        cancelCall(call);
    }

结束

相信到这边应该能满足Demo的要求了吧,至少用是可以用了,不过在实际开发中,这还是太糙了点,哎..没时间打磨,慢慢来吧,有兴趣的可以留言讨论下优化方案,大家拓展下思维也是不错的嘛

Android随笔
Web note ad 1