美文网首页Android ClassAndroid开发成长Rx系列
真实案例出发,再谈retrofit封装

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

作者: 程序员Anthony | 来源:发表于2016-10-13 18:00 被阅读10363次

    原文链接: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<List<Repo>> 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<List<Repo>> 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<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) {
    
        }
     });
    

    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<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);
        }
      }
    

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

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

     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);
              }
            });
      }
    

    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<List<GithubUser>>() {
                        @Override
                        public void onNext(List<GithubUser> users) {
                            ......Github用户数据加载完成
                        }
                    });
    

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

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

        /**
         * 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());
        }
    

    这里传入了接口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 接口,你可以直接访问
    https://api.github.com/users/CameloeAnthony/following 获取到这些列表数据。

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

    GithubUser 是与Github API对应的Github用户信息的实体类,API 和实体类的转化可以去网站http://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<WeatherData>() {
                @Override
                public void onNext(WeatherData weatherData) {
                 .......天气数据加载完成
                }
    
                @Override
                public void onError(Throwable e) {
                    super.onError(e);
                    toastUtils.showToast("加载天气信息失败");
                }
            });
    

    接着看看DataManager提供的方法 loadWeatherData

        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());
        }
    

    这里同样是调用了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<WeatherData> loadWeatherData(@QueryMap Map<String,String> params);
    }
    

    WeatherData同样是根据百度天气API编写的实体类 。这个实体类也是有点复杂。所以同样是通过http://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
     * <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;
            }
        }
    }
    

    这里getService方法将会获取缓存中的是否有传递进来的Class对象。有则使用,没有则创建。
    这里则调用了createService(Class<S> serviceClass)进行了OkHttpClient的初始化操作,并添加了两个LogInterceptor,CacheControlInterceptor 分别用于打印相关的请求信息。最终调用的方法是createService(Class<S> serviceClass, OkHttpClient client) 我们通过反射达到了end_point字段的基地址。比如上面的https://api.github.com/http://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 使用http://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深入浅出-终极封装

    相关文章

      网友评论

      • ChineseBoy:用retrofit写很多图文上传的接口的时候,太坑了
      • 奔跑的猴子:你好建哥,我想问一下你的女朋友在我这,我该怎么办??
      • idioitcbear:如果app有多种数据源要怎么做配置,例如,后台返回的结果有xml,又有json的话。
        西北狼神:@idioitcbear 那这个梗就是你们后台太渣。:smile: 这种后台,该怼还是的怼起来
        idioitcbear:@西北狼神 我不是这个意思,我的app后台,有的接口返回xml,有的接口返回json,不是说,一个接口可以根据我传的参数,返回对应的格式的数据。GET IT?
        西北狼神:后台返回xml or json,你都没看懂这个梗就来问。人家是后台同时支持返回某一种格式的信息,默认只返回其中一种比如json,另外一种比如xml的话你就要多传一个参数告诉他你想要什么格式的返回数据:joy:
      • 最最最最醉人:大兄弟是成都的啊
        程序员Anthony:@最最最最醉人 可以加我QQ 838577576 认识一下咯
        最最最最醉人:@CameloeAnthony 是啊,以后多交流交流 :grin:
        程序员Anthony:@最最最最醉人 对呀 。哈哈。 你也是吗
      • AndroidHarry:封装的不错 有一点不明白你说的 就是缺点一 1 Api接口类中的基地址,必须按照"end_point"的形式提供。这是什么形式? 就是string吗?
        程序员Anthony:@android大神 因为后面是利用反射后去end_point字段。所以在接口类中,基地址就必须 String end_point = "基地址"; 这种形式提供
      • jjdxmashl:你好,请教一个问题,retrofit2我最近在使用,遇到一种情况是,post方式,带参数的图片上传问题,多参数多图片的情况有没有封装的,类似说说的那种,图片数量不固定的
        兜里有糖_sky:@jjdxmashl http://blog.csdn.net/arieshao/article/details/51734205 多图片上传.需要自己转换一下.可以去看看
        程序员Anthony:@jjdxmashl 暂时不怎么清楚这个问题咯
      • 巴图鲁:膜拜

      本文标题:真实案例出发,再谈retrofit封装

      本文链接:https://www.haomeiwen.com/subject/vrtbyttx.html