阅读 2751

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

原文链接:Anthony的简书博客
项目代码:CameloeAnthony/Ant

前言

在使用了一段时间的Retrofit之后,今天终于在这里讲解到了网络的部分。目前开源的HTTP 框架有很多,Volley,Android Async Http,以及OkHttp +Retrofit等。而我在自己的使用中选择了Retrofit,这里就从基础到原理,再到实例的方式,讲解我对Retrofit做出的一些封装和使用。来让你进一步的了解和掌握Retrofit的使用。

基础

Retrofit一个基于OkHttp的RESTFUL API请求工具。它是 Square 推出的 HTTP 框架,主要用于 Android 和 Java。Retrofit 将网络请求变成方法的调用,使用起来非常简洁方便。

A type-safe HTTP client for Android and Java

如果你还对Retrofit不了解,那么我建议你去官方文档了解一下。

Retrofit使用大体分为三个步骤
(1)Retrofit将HTTP API 转化成了Java接口的形式,所以首先我们会提供一个接口类GitHubService 。

public interface GitHubService {
  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);
}复制代码

(2)Retrofit类可以针对之前定义的GitHubService 接口生成一个具体实现。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);复制代码

(3)然后就可以对GitHubService 的方法进行同步或者异步的调用来进行网络的访问,也就是说可以通过call对象获得数据:(可以使用enqueue 或者 execute来执行发起请求,enqueue是是异步执行,而 execute是同步执行。)

Call> repos = service.listRepos("octocat");复制代码

通过上面三个步骤,我们会发现Retrofit给人眼前一亮的当然是它的注解调用和优雅的API转化为方法。每一个方法都会对应着一个Http的注解,总共有GET, POST, PUT, DELETE,HEAD五个内嵌的注解。我们也会在注解上指定相应的相对地址信息。比如上方的@GET("users/{user}/repos")

这里本来想将官网所有内容翻译一遍的,返现很多词不达意 。然后今天又凑巧看到了郭神公众号推荐的一篇文章 Android网络请求库 - Say hello to retrofit.对官网对的内容讲解得非常的详细易懂,继续阅读下面章节之前,一定要去看看这篇文章。

于是我们完整的Retrofit使用流程:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
//.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();

GitHubService service = retrofit.create(GitHubService.class);
Call> repos = service.listRepos("octocat");
repos.enqueue(new Callback>() {
    @Override
    public void onResponse(Call> call, Response> response) {

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

    }
 });复制代码

Retrofit原理解析

在进一步了解和使用Retrofit之前,不妨先来了解Retrofit的原理,看看Retrofit的源码是了解原理的一个有效途径。

(1) 源码结构
Retrofit包含一个http包,里面全部是定义HTTP请求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等。
余下的retrofit包中几个类和接口就是全部retrofit的代码了,代码很少因为retrofit把网络请求这部分功能全部交给了OkHttp了。



(2) 整体流程

继续回到官网的例子。

首先关注的是我们通过new Retrofit.Builder()...build()进行Retrofit的构建,可以了解的是这里使用的是 Builder 模式。

在Android源码中,经常用到Builder模式的可能就是AlerDialog 了。Builder模式用于将一个复杂的对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。这里Retrofit使用Builder模式支持了支持不同的转换(就是将HTTP返回的数据解析成Java对象,主要有Xml、Gson等)和返回(主要作用就是将Call对象转换成另一个对象,比如RxJava)。这里也就真正的达到了构建复杂对象和它的部件进行解耦。

这里通过build方法来了解Retrofit创建,需要6个参数。如下方代码注解:

   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 adapterFactories = new ArrayList<>(this.adapterFactories);
      adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

      //5  converterFactories(例如GsonConverterFactory 用于Gson转换)  请求网络得到的response的转换器的集合 默认会加入 BuiltInConverters ,
      List converterFactories = new ArrayList<>(this.converterFactories);

      //6 private boolean validateEagerly;  validateEagerly 是否需要立即解析接口中的方法

      return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
          callbackExecutor, validateEagerly);
    }
  }复制代码

所以我们会看到我们通过Builder模式创建Retrofit访问对象都必须指定基地址url。如果还需要支持Gson转换,我们就需要添加.addConverterFactory(GsonConverterFactory.create()),如果需要支持Rxjava,那么还需要添加 .addCallAdapterFactory (RxJavaCallAdapterFactory.create())

接着我们通过GitHubService service = retrofit.create(GitHubService.class); create方法创建网络请求接口类GitHubService 的实例。我们正是使用该对象的listRepos方法完成了Call> repos = service.listRepos("octocat"); 获取到了数据。下面看看create方法的源码:

 public  T create(final Class 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);
          }
        });
  }复制代码

create方法接受一个 Class 对象,也就是我们编写的接口,里面含有通过注解标识的请求网络的方法。注意 return
语句部分,这里调用了 Proxy.newProxyInstance方法,这个很重要,因为用了动态代理模式。关于动态代理模式,可以参考这篇文章:公共技术点之 Java 动态代理。简单的描述就是,Proxy.newProxyInstance根据传进来的 Class 对象生成了一个实例 A,也就是代理类。每当这个代理类 A 执行某个方法时,总是会调用 InvocationHandler(Proxy.newProxyInstance中的第三个参数) 的invoke方法,在这个方法中可以执行一些操作(这里是解析方法的注解参数等),通过这个方法真正的执行我们编写的接口中的网络请求。

也就是概括一句话:通过动态代理的方式把 Java 接口中的解析为响应的网络请求,然后交给 OkHttp 去执行。并且可以适配不同的 CallAdapter
,可以方便与 RxJava 结合使用。

更多源码细节,参考这篇文章。Android Retrofit源码解析

封装和使用

之前有网友评论,说网络上的很多开源库已经封装的很完美了 ,我们就不需要再次做出多余的封装了 。这个观点实在是不敢苟同,开源库固然已经做了很多事情,但是我们还是要根据不同的业务逻辑封装自己的使用呢 。比如同样的图片加载,我们不可能每次都要调用Glide的一些初始化操作。同样的网络请求,我们也不可能每次都写一大堆初始化代码 。每个app的逻辑业务操作都是相同的,当然可以封装起来,让代码更加清爽。

下面讲讲我这里的封装逻辑。并提供Github我的关注列表以及百度天气接口的访问,两个真实案例进行讲解使用,项目代码将会在CameloeAnthony/Ant 中提供更新:

(1) Github 关注列表



来看看Github访问页面,这里只需要下面几行代码就完成了GithubUser 数据的返回。

        mSubscription = mDataManager.loadUserFollowingList("CameloeAnthony")
                .subscribe(new HttpSubscriber>() {
                    @Override
                    public void onNext(List users) {
                        ......Github用户数据加载完成
                    }
                });复制代码

这里使用loadUserFollowingList方法通过Rxjava的Observable返回Observable> 对象,DataManager是一个数据的入口,我们不将所有的数据访问放在DataManager中。这种创建方式在之前的文章浅析MVP中model层设计中有过提及,类似于通常使用的 Respository

接下来看看DataManager中的loadUserFollowingList方法。

    /**
     * load  following list of github users
     * @return Observable>
     */
    public Observable> loadUserFollowingList(String userName){
        return mHttpHelper.getService(GithubApi.class)
                .loadUserFollowingList(userName)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }复制代码

这里传入了接口GithubApi.class 然后调用了HttpHelper的loadUserFollowingList方法。

在此架构中,Model层被划分为两个部分:许多helpers类和一个 DataManager
.helpers类的数量在不同的工程中不尽相同,但是每个都有自己的功能。比如:通过SharedPreferences与数据进行交互的PreferHelper,通过SqlBrite提供与数据库交互的DatabaseHelper,DataManager结合并且转化不同的Helpers类为Rx操作符,向Presenter层提供Observables类型的数(provide meaningful data to the Presenter),并且同时处理数据的并发操作(group actions that will always happen together.)。这一层也包含实际的model类,用于定义当前数据架构。 -------摘选自浅析MVP中model层设计


GithubApi 接口,你可以直接访问
api.github.com/users/Camel… 获取到这些列表数据。

public interface GithubApi {
    String end_point = "https://api.github.com/";
    @GET("/users/{user}/following")
    Observable> loadUserFollowingList(@Path(value = "user") String user);
}复制代码

GithubUser 是与Github API对应的Github用户信息的实体类,API 和实体类的转化可以去网站www.jsonschema2pojo.org/ 快捷完成:

public class GithubUser {

    @SerializedName("login")
    private String login;
    @SerializedName("id")
    private Integer id;
......

    public String getLogin() {
        return login;
    }
......复制代码

(2) 天气信息的加载
这里加载百度API提供的天气信息


首先还是加载的页面的方法,非常简单的Rxjava操作完成了数据的读取

  mSubscription = mDataManager.loadWeatherData(“成都”).subscribe(new HttpSubscriber() {
            @Override
            public void onNext(WeatherData weatherData) {
             .......天气数据加载完成
            }

            @Override
            public void onError(Throwable e) {
                super.onError(e);
                toastUtils.showToast("加载天气信息失败");
            }
        });复制代码

接着看看DataManager提供的方法 loadWeatherData

    public Observable loadWeatherData(String location) {
        Map 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());
    }复制代码

这里同样是调用了HttpHelper的loadWeatherData方法。遵从的是上面一样的Model层访问原则。所有数据都是先访问DataManager再访问相应的类,比如这里的HttpHelper。

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 loadWeatherData(@QueryMap Map params);
}复制代码

WeatherData同样是根据百度天气API编写的实体类 。这个实体类也是有点复杂。所以同样是通过www.jsonschema2pojo.org/ 把API json放到输入框,然后写好名字,快速的完成了实体类的创建。


所以这里就讲解完了两个接口的调用 。但是客官,你肯定要说 ,这不对呀 ,你这Retrofit踪迹都没看到。你就完成了各种API调用,你逗我呢吧?哈哈,接着往下看。

(3)retrofit封装
这里还是要回到最初的那一段代码,retrofit的创建分为三个步骤。回到基础部分再看一下吧。虽然上面的两次API调用都没有“使用Retrofit”,但是都是使用了HttpHelper类。将GithubApiWeatherApi分别传递到了HttpHelper对象的getService方法中,所以猫腻就在这里 。看下面的代码:

/**
 * Created by Anthony on 2016/7/8.
 * Class Note:
 * entrance class to access network with {@link Retrofit}
 * used only by{@link DataManager} is recommended
 * < /code >< p >< code>
 * 使用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;
        }
    }
}复制代码

这里getService方法将会获取缓存中的是否有传递进来的Class对象。有则使用,没有则创建。
这里则调用了createService(Class serviceClass)进行了OkHttpClient的初始化操作,并添加了两个LogInterceptor,CacheControlInterceptor 分别用于打印相关的请求信息。最终调用的方法是createService(Class serviceClass, OkHttpClient client) 我们通过反射达到了end_point字段的基地址。比如上面的api.github.com/apis.baidu.com/。
然后代码也就回到了

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(end_point)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(client)
                .build();

        return retrofit.create(serviceClass);复制代码

可以看到了这里同样是进行了Retrofit的Builder创建以及create操作。到这里我想你就.addConverterFactory(GsonConverterFactory.create())完成了添加Gson的转换器的操作。RxJavaCallAdapterFactory.create() 完成了RxJava结果返回的支持。所以我们才可以支持了返回Observable的对象。

(4) 思路梳理

这里所有的代码也就完成了我们的操作。所以能够看到网络的访问变得更加简洁。这里对优点和缺点进行总结,需要在开发使用中注意:

优点:

1 通过DataManager作为数据入口的形式,屏蔽底层细节,让网络的访问更加清晰。

2 使用www.jsonschema2pojo.org/ 网站快速的通过API Json数据完成了实体类的创建

3 支持Rxjava的流式操作,是Retrofit的使用更加得心应手。当我们执行异步操作的时候,java提供了Thread, Future,FutureTask, CompletableFuture 去解决这个问题,但是随着任务的复杂程度的增加,代码也变得难于维护,他们也不能实现Rxjava一样的链式处理操作。Rxjava相比具有更高的灵活性,可以链式调用,可以对单个事件以及序列进行操作。

4 支持Gson转换器和Retrofit的配合,省去了Gson fromJson的操作。更加便捷。

5 HttpHelper对Retrofit的封装省去了Retrofit初始化的创建,并且添加了拦截器进行日志打印方便查看。

缺点:

1 Api接口类中的基地址,必须按照"end_point"的形式提供。

2 DataManager随着项目的增大作为唯一的数据入口将会变得越来越臃肿。

当然本项目也引入了Dagger2和ButterKnife,让代码更加的整洁易用。

当然本篇文章没有对官网和源码细节进行进一步解析。都可以在上面和下方提供的参考链接中进行查看。更多代码细节请查看

CameloeAnthony/Ant
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
由作者长期维护的架构以及示例代码,用于本人的各种真实项目。博客更新,希望对你的安卓架构提供指导性的意义

参考文章

Retrofit官网
Android网络请求库 - Say hello to retrofit
RxJava 与 Retrofit 结合的最佳实践
「Android技术汇」Retrofit2 源码解析和案例说明
Android Retrofit源码解析
Rxjava+ReTrofit+okHttp深入浅出-终极封装

文章分类
Android