Android 手把手教你使用Retrofit2
lixuqu
8年前
<h2>Android 手把手教你使用Retrofit2</h2> <h2>前言:</h2> <p>1.我想通过本文,让大家能更快的上手Retrofit2,跟上时代的步伐。</p> <p>2.本文主要是写给初级、中级开发人员阅读,所以在这边不会做太多的解释说明,只针对一些坑以及使用方法进行说明。</p> <p>3.本文只涉及到Retrofit2,并未与RxJava一起使用,RxJava+Retrofit2技术将会在下篇文章中呈现。</p> <p>4.本人也是刚开始接触Retrofit2,所以可能文章中会有一些错误或者不足,希望大家多多留言探讨,共同进步</p> <h2>开启Retrofit2之旅</h2> <h3>添加依赖</h3> <p>以下是我用到的一些依赖</p> <pre> <code class="language-java">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'</code></pre> <h3>注解</h3> <p>retrofit通过使用注解来简化请求,大体分为以下几类:</p> <p>1.用于标注请求方式的注解</p> <p>2.用于标记请求头的注解</p> <p>3.用于标记请求参数的注解</p> <p>4.用于标记请求和响应格式的注解</p> <p>请求方法注解</p> <table> <thead> <tr> <th>注解</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>@GET</td> <td>get请求</td> </tr> <tr> <td>@POST</td> <td>post请求</td> </tr> <tr> <td>@PUT</td> <td>put请求</td> </tr> <tr> <td>@DELETE</td> <td>delete请求</td> </tr> <tr> <td>@PATCH</td> <td>patch请求,该请求是对put请求的补充,用于更新局部资源</td> </tr> <tr> <td>@HEAD</td> <td>head请求</td> </tr> <tr> <td>@OPTIONS</td> <td>option请求</td> </tr> <tr> <td>@HTTP</td> <td>通用注解,可以替换以上所有的注解,其拥有三个属性:method,path,hasBody</td> </tr> </tbody> </table> <p>请求头注解</p> <table> <thead> <tr> <th>注解</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>@Headers</td> <td>用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在</td> </tr> <tr> <td>@Header</td> <td>作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头</td> </tr> </tbody> </table> <p>请求参数注解</p> <table> <thead> <tr> <th>名称</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>@Body</td> <td>多用于post请求发送非表单数据,比如想要以post方式传递json格式数据</td> </tr> <tr> <td>@Filed</td> <td>多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用</td> </tr> <tr> <td>@FiledMap</td> <td>和@Filed作用一致,用于不确定表单参数</td> </tr> <tr> <td>@Part</td> <td>用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况</td> </tr> <tr> <td>@PartMap</td> <td>用于表单字段,默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传</td> </tr> <tr> <td>@Path</td> <td>用于url中的占位符</td> </tr> <tr> <td>@Query</td> <td>用于Get中指定参数</td> </tr> <tr> <td>@QueryMap</td> <td>和Query使用类似</td> </tr> <tr> <td>@Url</td> <td>指定请求路径</td> </tr> </tbody> </table> <p>请求和响应格式注解</p> <table> <thead> <tr> <th>名称</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>@FormUrlEncoded</td> <td>表示请求发送编码表单数据,每个键值对需要使用@Field注解</td> </tr> <tr> <td>@Multipart</td> <td>表示请求发送multipart数据,需要配合使用@Part</td> </tr> <tr> <td>@Streaming</td> <td>表示响应用字节流的形式返回.如果没使用该注解,默认会把数据全部载入到内存中.该注解在在下载大文件的特别有用</td> </tr> </tbody> </table> <p>使用</p> <p>这里不对注解进行过多的说明了,只展示几个用法</p> <pre> <code class="language-java">/** * 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); }</code></pre> <p>踩坑</p> <p>1.url被转义</p> <pre> <code class="language-java">http://api.mydemo.com/api%2Fnews%2FnewsList?</code></pre> <p>罪魁祸首@Url与@Path注解,我们开发过程中,肯定会需要动态的修改请求地址</p> <p>两种动态修改方式如下:</p> <pre> <code class="language-java">@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);</code></pre> <p>第一种是直接使用@Url,它相当于直接替换了@POST()里面的请求地址</p> <p>第二种是使用@Path("url"),它只替换了@POST("api/{url}/newsList")中的{url}</p> <p>如果你用下面这样写的话,就会出现url被转义</p> <pre> <code class="language-java">@POST("{url}") Call<HttpResult<News>> post(@Path("url") String url);</code></pre> <p>你如果执意要用@Path,也不是不可以,需要这样写</p> <pre> <code class="language-java">@POST("{url}") Call<HttpResult<News>> post(@Path(value = "url", encoded = true) String url);</code></pre> <h3>OkHttpClient</h3> <p>拦截器</p> <p>addNetworkInterceptor添加的是网络拦截器Network Interfacetor它会在request和response时分别被调用一次;</p> <p>addInterceptor添加的是应用拦截器Application Interceptor他只会在response被调用一次。</p> <p>1.日志拦截器</p> <p>使用 addNetworkInterceptor() 方法添加到 <strong>OkHttpClient</strong> 中</p> <p>日志拦截器我这有两种创建方式:</p> <p>一种是使用HttpLoggingInterceptor,需要使用到依赖</p> <pre> <code class="language-java">compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'</code></pre> <pre> <code class="language-java">/** * 创建日志拦截器 * * @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; }</code></pre> <p>另一种是</p> <pre> <code class="language-java">/** * 创建日志拦截器 * * @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; } }</code></pre> <p>2.请求头拦截器</p> <p>使用 addInterceptor() 方法添加到 <strong>OkHttpClient</strong> 中</p> <p>我的理解是,请求头拦截器是为了让服务端能更好的识别该请求,服务器那边通过请求头判断该请求是否为有效请求等...</p> <pre> <code class="language-java">/** * 拦截器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; }</code></pre> <p>3.统一请求拦截器</p> <p>使用 addInterceptor() 方法添加到 <strong>OkHttpClient</strong> 中</p> <p>统一请求拦截器的功能跟请求头拦截器相类似</p> <pre> <code class="language-java">/** * 拦截器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; }</code></pre> <p>4.时间拦截器</p> <p>使用 addInterceptor() 方法添加到 <strong>OkHttpClient</strong> 中</p> <p>从响应中获取服务器返回的时间</p> <pre> <code class="language-java">/** * 拦截器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; }</code></pre> <h3>缓存</h3> <p>使用okhttp缓存的话,先要创建Cache,然后在创建缓存拦截器</p> <pre> <code class="language-java">//创建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());</code></pre> <p>缓存拦截器</p> <p>缓存时间自己根据情况设定</p> <pre> <code class="language-java">/** * 在无网络的情况下读取缓存,有网络的情况下根据缓存的过期时间重新请求, * * @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; }</code></pre> <h3>自定义CookieJar</h3> <pre> <code class="language-java">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>(); } });</code></pre> <h2>启动Retrofit2</h2> <p>到了这里,基本上准备工作都做好了,可以启动Retrofit2了</p> <pre> <code class="language-java">retrofit = new Retrofit.Builder() .client(httpClientBuilder.build()) .baseUrl(BASE_URL) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(FastJsonConverterFactory.create()) .build(); mApi = retrofit.create(MyRetrofit2Service.class);</code></pre> <p>界面上通过getEnqueue()方法调用</p> <pre> <code class="language-java">/** * 异步调用 */ 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); }</code></pre> <h2>结束</h2> <p>相信到这边应该能满足Demo的要求了吧,至少用是可以用了,不过在实际开发中,这还是太糙了点,哎..没时间打磨,慢慢来吧,有兴趣的可以留言讨论下优化方案,大家拓展下思维也是不错的嘛</p> <p> </p> <p>来自:http://www.jianshu.com/p/73216939806a</p> <p> </p>