真实案例出发,再谈retrofit封装

nk2670 8年前
   <h2><strong>前言</strong></h2>    <p>在使用了一段时间的Retrofit之后,今天终于在这里讲解到了网络的部分。目前开源的HTTP 框架有很多,Volley,Android Async Http,以及OkHttp +Retrofit等。而我在自己的使用中选择了Retrofit,这里就从基础到原理,再到实例的方式,讲解我对Retrofit做出的一些封装和使用。来让你进一步的了解和掌握Retrofit的使用。</p>    <h2><strong>基础</strong></h2>    <p>Retrofit一个基于OkHttp的RESTFUL API请求工具。它是 Square 推出的 HTTP 框架,主要用于 Android 和 Java。Retrofit 将网络请求变成方法的调用,使用起来非常简洁方便。</p>    <p>A type-safe HTTP client for Android and Java</p>    <p>如果你还对Retrofit不了解,那么我建议你去 <a href="/misc/goto?guid=4958837204152834453" rel="nofollow,noindex">官方文档</a> 了解一下。</p>    <p>Retrofit使用大体分为三个步骤</p>    <p>(1)Retrofit将HTTP API 转化成了Java接口的形式,所以首先我们会提供一个接口类GitHubService 。</p>    <pre>  <code class="language-java">public interface GitHubService {    @GET("users/{user}/repos")    Call<List<Repo>> listRepos(@Path("user") String user);  }</code></pre>    <p>(2)Retrofit类可以针对之前定义的GitHubService 接口生成一个具体实现。</p>    <pre>  <code class="language-java">Retrofit retrofit = new Retrofit.Builder()      .baseUrl("https://api.github.com/")      .build();    GitHubService service = retrofit.create(GitHubService.class);</code></pre>    <p>(3)然后就可以对GitHubService 的方法进行同步或者异步的调用来进行网络的访问,也就是说可以通过call对象获得数据:(可以使用enqueue 或者 execute来执行发起请求,enqueue是是异步执行,而 execute是同步执行。)</p>    <pre>  <code class="language-java">Call<List<Repo>> repos = service.listRepos("octocat");</code></pre>    <p>通过上面三个步骤,我们会发现Retrofit给人眼前一亮的当然是它的注解调用和优雅的API转化为方法。每一个方法都会对应着一个Http的注解,总共有GET, POST, PUT, DELETE,HEAD五个内嵌的注解。我们也会在注解上指定相应的相对地址信息。比如上方的 @GET("users/{user}/repos")</p>    <p>这里本来想将官网所有内容翻译一遍的,返现很多词不达意 。然后今天又凑巧看到了郭神公众号推荐的一篇文章 Android网络请求库 - Say hello to retrofit .对官网对的内容讲解得非常的详细易懂,继续阅读下面章节之前,一定要去看看这篇文章。</p>    <p>于是我们完整的Retrofit使用流程:</p>    <pre>  <code class="language-java">Retrofit retrofit = new Retrofit.Builder()  .baseUrl("https://api.github.com/")  .addConverterFactory(GsonConverterFactory.create())  //.addCallAdapterFactory(RxJavaCallAdapterFactory.create())  .build();    GitHubService service = retrofit.create(GitHubService.class);  Call<List<Repo>> repos = service.listRepos("octocat");  repos.enqueue(new Callback<List<Repo>>() {      @Override      public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {        }      @Override      public void onFailure(Call<List<Repo>> call, Throwable t) {        }   });</code></pre>    <h2><strong>Retrofit原理解析</strong></h2>    <p>在进一步了解和使用Retrofit之前,不妨先来了解Retrofit的原理,看看Retrofit的源码是了解原理的一个有效途径。</p>    <p>(1) 源码结构</p>    <p>Retrofit包含一个http包,里面全部是定义HTTP请求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等。</p>    <p>余下的retrofit包中几个类和接口就是全部retrofit的代码了,代码很少因为retrofit把网络请求这部分功能全部交给了OkHttp了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fbcd3d0c129b29590aab8a9862bccd23.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/383b06a45cd0e48e21ca310ab76b5c82.png"></p>    <p>(2) 整体流程</p>    <p>继续回到官网的例子。</p>    <p>首先关注的是我们通过 new Retrofit.Builder()...build() 进行Retrofit的构建,可以了解的是这里使用的是 Builder 模式。</p>    <p>在Android源码中,经常用到Builder模式的可能就是AlerDialog 了。Builder模式用于将一个复杂的对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。这里Retrofit使用Builder模式支持了支持不同的转换(就是将HTTP返回的数据解析成Java对象,主要有Xml、Gson等)和返回(主要作用就是将Call对象转换成另一个对象,比如RxJava)。这里也就真正的达到了构建复杂对象和它的部件进行解耦。</p>    <p>这里通过build方法来了解Retrofit创建,需要6个参数。如下方代码注解:</p>    <pre>  <code class="language-java">public Retrofit build() {        //1 baseUrl   基地址        if (baseUrl == null) {          throw new IllegalStateException("Base URL required.");        }        //2 callFactory  默认创建一个 OkHttpClient        okhttp3.Call.Factory callFactory = this.callFactory;        if (callFactory == null) {          callFactory = new OkHttpClient();        }          Executor callbackExecutor = this.callbackExecutor;        if (callbackExecutor == null) {          //3 callbackExecutor  Android 中返回的是 MainThreadExecutor          callbackExecutor = platform.defaultCallbackExecutor();        }         //4 adapterFactories(比如RxJavaCallAdapterFactory 用于将Call返回支持Rxjava)  把Call对象转换成其它类型        List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);        adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));          //5  converterFactories(例如GsonConverterFactory 用于Gson转换)  请求网络得到的response的转换器的集合 默认会加入 BuiltInConverters ,        List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);          //6 private boolean validateEagerly;  validateEagerly 是否需要立即解析接口中的方法          return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,            callbackExecutor, validateEagerly);      }    }</code></pre>    <p>所以我们会看到我们通过Builder模式创建Retrofit访问对象都必须指定基地址url。如果还需要支持Gson转换,我们就需要添加 .addConverterFactory(GsonConverterFactory.create()) ,如果需要支持Rxjava,那么还需要添加 .addCallAdapterFactory (RxJavaCallAdapterFactory.create()) 。</p>    <p>接着我们通过 GitHubService service = retrofit.create(GitHubService.class); create方法创建网络请求接口类GitHubService 的实例。我们正是使用该对象的listRepos方法完成了 Call<List<Repo>> repos = service.listRepos("octocat"); 获取到了数据。下面看看create方法的源码:</p>    <pre>  <code class="language-java">public <T> T create(final Class<T> service) {      Utils.validateServiceInterface(service);      if (validateEagerly) {        eagerlyValidateMethods(service);      }      return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },          new InvocationHandler() {            private final Platform platform = Platform.get();              @Override public Object invoke(Object proxy, Method method, Object... args)                throws Throwable {              // If the method is a method from Object then defer to normal invocation.              if (method.getDeclaringClass() == Object.class) {                return method.invoke(this, args);              }              //为了兼容 Java8 平台,Android 中不会执行              if (platform.isDefaultMethod(method)) {                return platform.invokeDefaultMethod(method, service, proxy, args);              }              ServiceMethod serviceMethod = loadServiceMethod(method);              OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);              return serviceMethod.callAdapter.adapt(okHttpCall);            }          });    }</code></pre>    <p>create方法接受一个 Class 对象,也就是我们编写的接口,里面含有通过注解标识的请求网络的方法。注意 return</p>    <p>语句部分,这里调用了 Proxy.newProxyInstance方法,这个很重要,因为用了 <strong>动态代理模式</strong> 。关于动态代理模式,可以参考这篇文章: <a href="/misc/goto?guid=4959720672163494623" rel="nofollow,noindex">公共技术点之 Java 动态代理</a> 。简单的描述就是,Proxy.newProxyInstance根据传进来的 Class 对象生成了一个实例 A,也就是代理类。每当这个代理类 A 执行某个方法时,总是会调用 InvocationHandler(Proxy.newProxyInstance中的第三个参数) 的invoke方法,在这个方法中可以执行一些操作(这里是解析方法的注解参数等),通过这个方法真正的执行我们编写的接口中的网络请求。</p>    <p>也就是概括一句话:通过动态代理的方式把 Java 接口中的解析为响应的网络请求,然后交给 OkHttp 去执行。并且可以适配不同的 CallAdapter</p>    <p>可以方便与 RxJava 结合使用。</p>    <h2><strong>封装和使用</strong></h2>    <p>之前有网友评论,说网络上的很多开源库已经封装的很完美了 ,我们就不需要再次做出多余的封装了 。这个观点实在是不敢苟同,开源库固然已经做了很多事情,但是我们还是要根据不同的业务逻辑封装自己的使用呢 。比如同样的图片加载,我们不可能每次都要调用Glide的一些初始化操作。同样的网络请求,我们也不可能每次都写一大堆初始化代码 。每个app的逻辑业务操作都是相同的,当然可以封装起来,让代码更加清爽。</p>    <p>下面讲讲我这里的封装逻辑。并提供Github我的关注列表以及百度天气接口的访问,两个真实案例进行讲解使用,项目代码将会在 CameloeAnthony / <strong> Ant </strong> 中提供更新:</p>    <p>(1) Github 关注列表</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f3dacd05b646f17377a6d2f1c82b4389.png"></p>    <p>来看看Github访问页面,这里只需要下面几行代码就完成了GithubUser 数据的返回。</p>    <pre>  <code class="language-java">mSubscription = mDataManager.loadUserFollowingList("CameloeAnthony")                  .subscribe(new HttpSubscriber<List<GithubUser>>() {                      @Override                      public void onNext(List<GithubUser> users) {                          ......Github用户数据加载完成                      }                  });</code></pre>    <p>这里使用 loadUserFollowingList 方法通过Rxjava的Observable返回 Observable<List<GithubUser>> 对象,DataManager是一个数据的入口,我们不将所有的数据访问放在DataManager中。这种创建方式在之前的文章 <a href="/misc/goto?guid=4959674442599349744" rel="nofollow,noindex">浅析MVP中model层设计</a> 中有过提及,类似于通常使用的 Respository</p>    <p>接下来看看DataManager中的 loadUserFollowingList 方法。</p>    <pre>  <code class="language-java">/**       * load  following list of github users       * @return Observable<List<GithubUser>>       */      public Observable<List<GithubUser>> loadUserFollowingList(String userName){          return mHttpHelper.getService(GithubApi.class)                  .loadUserFollowingList(userName)                  .subscribeOn(Schedulers.io())                  .observeOn(AndroidSchedulers.mainThread());      }</code></pre>    <p>这里传入了接口GithubApi.class 然后调用了HttpHelper的 loadUserFollowingList 方法。</p>    <p>在此架构中,Model层被划分为两个部分:许多helpers类和一个 DataManager</p>    <p>.helpers类的数量在不同的工程中不尽相同,但是每个都有自己的功能。比如:通过SharedPreferences与数据进行交互的PreferHelper,通过SqlBrite提供与数据库交互的DatabaseHelper,DataManager结合并且转化不同的Helpers类为Rx操作符,向Presenter层提供Observables类型的数(provide meaningful data to the Presenter),并且同时处理数据的并发操作(group actions that will always happen together.)。这一层也包含实际的model类,用于定义当前数据架构。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2a260a4e347fdba61022f024dde9b2c3.png"></p>    <p>GithubApi 接口,你可以直接访问</p>    <p>https://api.github.com/users/CameloeAnthony/following 获取到这些列表数据。</p>    <pre>  <code class="language-java">public interface GithubApi {      String end_point = "https://api.github.com/";      @GET("/users/{user}/following")      Observable<List<GithubUser>> loadUserFollowingList(@Path(value = "user") String user);  }</code></pre>    <p>GithubUser 是与Github API对应的Github用户信息的实体类,API 和实体类的转化可以去网站 http://www.jsonschema2pojo.org/ 快捷完成:</p>    <pre>  <code class="language-java">public class GithubUser {        @SerializedName("login")      private String login;      @SerializedName("id")      private Integer id;  ......        public String getLogin() {          return login;      }  ......</code></pre>    <p>(2) 天气信息的加载</p>    <p>这里加载百度API提供的天气信息</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3209f40674ddfcd1f2f14f5831bc96e6.png"></p>    <p>首先还是加载的页面的方法,非常简单的Rxjava操作完成了数据的读取</p>    <pre>  <code class="language-java">mSubscription = mDataManager.loadWeatherData(“成都”).subscribe(new HttpSubscriber<WeatherData>() {              @Override              public void onNext(WeatherData weatherData) {               .......天气数据加载完成              }                @Override              public void onError(Throwable e) {                  super.onError(e);                  toastUtils.showToast("加载天气信息失败");              }          });</code></pre>    <p>接着看看DataManager提供的方法 loadWeatherData</p>    <pre>  <code class="language-java">public Observable<WeatherData> loadWeatherData(String location) {          Map<String, String> params = new HashMap<>();          params.put("location", location);          params.put("language", "zh-Hans");          params.put("unit", "c");          params.put("start", "0");          params.put("days", "3");          return mHttpHelper.getService(WeatherApi.class)                  .loadWeatherData(params)                  .subscribeOn(Schedulers.io())                  .observeOn(AndroidSchedulers.mainThread());      }</code></pre>    <p>这里同样是调用了HttpHelper的loadWeatherData方法。遵从的是上面一样的Model层访问原则。所有数据都是先访问DataManager再访问相应的类,比如这里的HttpHelper。</p>    <pre>  <code class="language-java">public interface WeatherApi {      String end_point = “http://apis.baidu.com/”;        //example , remember to add a apikey to your header  // "http://apis.baidu.com/thinkpage/weather_api/suggestion?location=beijing&language=zh-Hans&unit=c&start=0&days=3";        @Headers("apikey: 87f4cacc3ffe1f1025ebf1ea415ff112")      @GET("/thinkpage/weather_api/suggestion")      Observable<WeatherData> loadWeatherData(@QueryMap Map<String,String> params);  }</code></pre>    <p>WeatherData同样是根据百度天气API编写的实体类 。这个实体类也是有点复杂。所以同样是通过 <a href="/misc/goto?guid=4958832210121601500" rel="nofollow,noindex">http://www.jsonschema2pojo.org/</a> 把API json放到输入框,然后写好名字,快速的完成了实体类的创建。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2f93453630890af86a46f7075c3a4642.png"></p>    <p>所以这里就讲解完了两个接口的调用 。但是客官,你肯定要说 ,这不对呀 ,你这Retrofit踪迹都没看到。你就完成了各种API调用,你逗我呢吧?哈哈,接着往下看。</p>    <p>(3)retrofit封装</p>    <p>这里还是要回到最初的那一段代码,retrofit的创建分为三个步骤。回到基础部分再看一下吧。虽然上面的两次API调用都没有“使用Retrofit”,但是都是使用了HttpHelper类。将 GithubApi 和 WeatherApi 分别传递到了HttpHelper对象的 getService 方法中,所以猫腻就在这里 。看下面的代码:</p>    <pre>  <code class="language-java">/**   * Created by Anthony on 2016/7/8.   * Class Note:   * entrance class to access network with {@link Retrofit}   * used only by{@link DataManager} is recommended   * <p>   * 使用retrofit进行网络访问的入口类,推荐只在{@link DataManager}中使用   */  public class HttpHelper {      private static final int DEFAULT_TIMEOUT = 30;      private HashMap<String, Object> mServiceMap;      private Context mContext;  //    private Gson gson = new GsonBuilder().setLenient().create();        @Inject      public HttpHelper(@ApplicationContext Context context) {          //Map used to store RetrofitService          mServiceMap = new HashMap<>();          this.mContext = context;      }          @SuppressWarnings("unchecked")      public <S> S getService(Class<S> serviceClass) {          if (mServiceMap.containsKey(serviceClass.getName())) {              return (S) mServiceMap.get(serviceClass.getName());          } else {              Object obj = createService(serviceClass);              mServiceMap.put(serviceClass.getName(), obj);              return (S) obj;          }      }        @SuppressWarnings("unchecked")      public <S> S getService(Class<S> serviceClass, OkHttpClient client) {          if (mServiceMap.containsKey(serviceClass.getName())) {              return (S) mServiceMap.get(serviceClass.getName());          } else {              Object obj = createService(serviceClass, client);              mServiceMap.put(serviceClass.getName(), obj);              return (S) obj;          }      }        private <S> S createService(Class<S> serviceClass) {          //custom OkHttp          OkHttpClient.Builder httpClient = new OkHttpClient.Builder();          //time our          httpClient.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);          httpClient.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);          httpClient.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);          //cache          File httpCacheDirectory = new File(mContext.getCacheDir(), "OkHttpCache");          httpClient.cache(new Cache(httpCacheDirectory, 10 * 1024 * 1024));          //Interceptor          httpClient.addNetworkInterceptor(new LogInterceptor());          httpClient.addInterceptor(new CacheControlInterceptor());            return createService(serviceClass, httpClient.build());      }        private <S> S createService(Class<S> serviceClass, OkHttpClient client) {          String end_point = "";          try {              Field field1 = serviceClass.getField("end_point");              end_point = (String) field1.get(serviceClass);          } catch (NoSuchFieldException e) {              e.printStackTrace();          } catch (IllegalAccessException e) {              e.getMessage();              e.printStackTrace();          }            Retrofit retrofit = new Retrofit.Builder()                  .baseUrl(end_point)                  .addConverterFactory(GsonConverterFactory.create())                  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())                  .client(client)                  .build();            return retrofit.create(serviceClass);      }        private class LogInterceptor implements Interceptor {          @Override          public Response intercept(Chain chain) throws IOException {              Request request = chain.request();                long t1 = System.nanoTime();              Timber.i("HttpHelper" + String.format("Sending request %s on %s%n%s",                      request.url(), chain.connection(), request.headers()));                Response response = chain.proceed(request);              long t2 = System.nanoTime();                Timber.i("HttpHelper" + String.format("Received response for %s in %.1fms%n%s",                      response.request().url(), (t2 - t1) / 1e6d, response.headers()));              return response;                // log Response Body  //            if(BuildConfig.DEBUG) {  //                String responseBody = response.body().string();  //                Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s%n%s",  //                        response.request().url(), (t2 - t1) / 1e6d, response.headers(), responseBody));  //                return response.newBuilder()  //                        .body(ResponseBody.create(response.body().contentType(), responseBody))  //                        .build();  //            } else {  //                Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s",  //                        response.request().url(), (t2 - t1) / 1e6d, response.headers()));  //                return response;  //            }          }      }        private class CacheControlInterceptor implements Interceptor {          @Override          public Response intercept(Chain chain) throws IOException {              Request request = chain.request();              if (!AppUtils.isNetworkConnected(mContext)) {                  request = request.newBuilder()                          .cacheControl(CacheControl.FORCE_CACHE)                          .build();              }                Response response = chain.proceed(request);                if (AppUtils.isNetworkConnected(mContext)) {                  int maxAge = 60 * 60; // read from cache for 1 minute                  response.newBuilder()                          .removeHeader("Pragma")                          .header("Cache-Control", "public, max-age=" + maxAge)                          .build();              } else {                  int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale                  response.newBuilder()                          .removeHeader("Pragma")                          .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)                          .build();              }              return response;          }      }  }</code></pre>    <p>这里getService方法将会获取缓存中的是否有传递进来的Class对象。有则使用,没有则创建。</p>    <p>这里则调用了 createService(Class<S> serviceClass) 进行了OkHttpClient的初始化操作,并添加了两个LogInterceptor,CacheControlInterceptor 分别用于打印相关的请求信息。最终调用的方法是 createService(Class<S> serviceClass, OkHttpClient client) 我们通过反射达到了end_point字段的基地址。</p>    <p>然后代码也就回到了</p>    <pre>  <code class="language-java">Retrofit retrofit = new Retrofit.Builder()                  .baseUrl(end_point)                  .addConverterFactory(GsonConverterFactory.create())                  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())                  .client(client)                  .build();            return retrofit.create(serviceClass);</code></pre>    <p>可以看到了这里同样是进行了Retrofit的Builder创建以及create操作。到这里我想你就 .addConverterFactory(GsonConverterFactory.create()) 完成了添加Gson的转换器的操作。 RxJavaCallAdapterFactory.create() 完成了RxJava结果返回的支持。所以我们才可以支持了返回Observable的对象。</p>    <p>(4) 思路梳理</p>    <p>这里所有的代码也就完成了我们的操作。所以能够看到网络的访问变得更加简洁。这里对优点和缺点进行总结,需要在开发使用中注意:</p>    <p>优点:</p>    <p>1 通过DataManager作为数据入口的形式,屏蔽底层细节,让网络的访问更加清晰。</p>    <p>2 使用 快速的通过API Json数据完成了实体类的创建</p>    <p>3 支持Rxjava的流式操作,是Retrofit的使用更加得心应手。当我们执行异步操作的时候,java提供了Thread, Future,FutureTask, CompletableFuture 去解决这个问题,但是随着任务的复杂程度的增加,代码也变得难于维护,他们也不能实现Rxjava一样的链式处理操作。Rxjava相比具有更高的灵活性,可以链式调用,可以对单个事件以及序列进行操作。</p>    <p>4 支持Gson转换器和Retrofit的配合,省去了Gson fromJson的操作。更加便捷。</p>    <p>5 HttpHelper对Retrofit的封装省去了Retrofit初始化的创建,并且添加了拦截器进行日志打印方便查看。</p>    <p>缺点:</p>    <p>1 Api接口类中的基地址,必须按照”end_point”的形式提供。</p>    <p>2 DataManager随着项目的增大作为唯一的数据入口将会变得越来越臃肿。</p>    <p>当然本项目也引入了Dagger2和ButterKnife,让代码更加的整洁易用。</p>    <p>当然本篇文章没有对官网和源码细节进行进一步解析。都可以在上面和下方提供的参考链接中进行查看。</p>    <p>example project make your architecting of android apps quicker and smoother ,long-term maintenance by me,blog updating at the same time.Used in real project in my development 由作者长期维护的架构以及示例代码,用于本人的各种真实项目。博客更新,希望对你的安卓架构提供指导性的意义</p>    <h2><strong>参考文章</strong></h2>    <p><a href="/misc/goto?guid=4959671481386446029" rel="nofollow,noindex">Retrofit官网</a></p>    <p><a href="/misc/goto?guid=4959720672338624936" rel="nofollow,noindex">Android网络请求库 - Say hello to retrofit</a></p>    <p><a href="/misc/goto?guid=4959720672419461902" rel="nofollow,noindex">RxJava 与 Retrofit 结合的最佳实践</a> <a href="/misc/goto?guid=4959720672506730171" rel="nofollow,noindex">「Android技术汇」Retrofit2 源码解析和案例说明</a> <a href="/misc/goto?guid=4959720672581723948" rel="nofollow,noindex">Android Retrofit源码解析</a> <a href="/misc/goto?guid=4959720672667195106" rel="nofollow,noindex"> <strong>Rxjava+ReTrofit+okHttp深入浅出-终极封装</strong> </a></p>    <p> </p>    <p>来自:http://blog.csdn.net/u014315849/article/details/52809011</p>    <p> </p>