Rxjava操作符(defer,compose,retryWhen)

上一篇博客讲了Retrofit的简单使用,应该看过的都基本了解我们公司这个服务器请求网络数据的流程,我来简单梳理一下:

  • 第一次登录,创建cookiejar,请求服务器数据,保存accesstoken到本地
  • 请求其他网络数据,使用已存在的cookiejar,传入本地保存的accesstoken
  • 基本上是这样,但是在实际操作中,用户可能登录之后,过了很久才去请求其他数据,这时候cookie已经失效就需要有重试机制。

  • 请求接口数据
  • 如果服务器返回的状态码是登录超时,则需要在代码中实现自动重新登录,并保存新的cookie和新的accesstoken
  • 使用新的accesstoken和cookie再次请求该接口数据
  • 如果返回状态码是成功,直接进行后续操作
  • 需求已经梳理清楚了,如果要用okhttp来实现以上的这个功能,或者说用异步任务/Handler 来实现网络请求。
    重试这一步就显得异常艰难,直接导致了代码重用困难的问题。
    即使我们使用异步任务/Handler 实现了以上流程,我们也只能在每次请求的时候复制这部分的处理代码,因为对于重试这部分,每次请求的代码都不一样,难以抽取成通用代码。
    当然okhttp有重试机制,但是如果要通过判断请求结果状态,然后再执行一定操作,再重试, 用okhttp实现起来也是非常困难的。
    如果再增加一个重试次数限制,那可想而知,代码量会有多大了。

    但是,是用Rx我们能够轻松地对Obserable进行变换,判断,处理等等,前提是你得熟练Rx。Rx也提供了对变换的封装。我们能够将一系列相同的变换封装成一个Transform,这涉及到compose操作符。
    接下来我先简单介绍一下compose、defer、retryWhen这几个操作符。

    compose

    compose其实就是对Obserable进行一系列的变换。
    举个最简单的例子,基本上在网上搜索compose都能够搜索到这个例子。
    经常使用RxAndroid的小伙伴们应该知道,每次请求网络的时候必加的代码就是,线程切换。

    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())

    这两句代码其实我也是深恶痛绝的, 每次都写错,不过没关系,我们可以通过compose来简单封装一下这两句代码。

    <T> Transformer<T, T> applySchedulers() {
     return new Transformer<T, T>() {
       @Override
       public Observable<T> call(Observable<T> observable) {
         return observable.subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread());
       }
     };
    }

    写一个applySchedulers()方法,内部实现这个变换。
    使用的时候很简单。

    .compose(applySchedulers())

    这一句就实现了线程切换,而且不容易出错。这是compose最基本的用法,看了上面的例子,对compose应该有了基本的了解。

    defer

    defer其实就是每次产生一个新的Obserable。
    在我们创建Obserable的时候通常都会传递一些参数,有时候这个参数会在程序运行的过程中发生改变,当再次调用这个方法时,要使用最新的参数值,而不是原来的参数值。
    这么描述可能还是不知道它到底有什么用,待会会在例子中用上,相信你一定能对它有个比较深刻的理解。

    retryWhen

    retryWhen其实就是在某个条件达成的时候,重新调用Obserable中的方法。

    接下来我们实现一下这个cookie or token过期,自动重新登录,并重新请求数据,并最终封装成通用的方法。

    api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .flatMap(new Func1>, Observable>>>() {
                @Override
                public Observable>> call(RequestResult> listRequestResult) {
                    if (listRequestResult.getErrcode() == -1) {
                        System.out.println("cookie失效");
                        //如果请求到的数据 错误码是 -1,抛出一个运行时异常错误。
                        return Observable.error(new RuntimeException("cookieError"));
                    }
                    //否则,说明数据正常获取到了,直接将数据传递下去。
                    return Observable.just(listRequestResult);
                }
            })
            //重新发起请求,当...发生某个错误的时候。
            // 在上一个flatmap中我们抛出的是 运行时异常,内容是"cookieError"
            .retryWhen(observable -> {
                //由于接收到的是一个Obserable.error对象,我们要对这个错误进行处理,需要flatMap一下。
                return observable.flatMap(new Func1>() {
                    @Override
                    public Observable call(Throwable throwable) {
                        //当发生这个cookieError错误的时候,实现自动登录
                        if (throwable instanceof RuntimeException) {
                            return api.UserLogin("action","key","fancy","123456")
                                    .subscribeOn(Schedulers.io())
                                    .observeOn(AndroidSchedulers.mainThread())
                                    .flatMap(new Func1, Observable>() {
                                        @Override
                                        public Observable call(RequestResult stringRequestResult) {
                                            //自动登录后,把新的token保存下来。
                                            Dic.accessToken = stringRequestResult.getAccesstoken();
                                            //登录后,不需要为下一个步骤传递任何参数,所以直接使用 just就可以了。
                                            return Observable.just(1);
                                        }
                                    });
                        }
                        //发生其他错误,可以进行其他处理,这里就没做处理了,简单抛出一个非法参数错误。
                        // 最终会在 subscribe中的onError方法中得到统一处理
                        return Observable.error(new IllegalArgumentException("没救了"));
                    }
                });
            })
            .subscribe(new ServerSubscriber>>() {
                @Override
                public void onNext(RequestResult> listRequestResult) {
                    //输出结果
                    System.out.println(listRequestResult);
                }
            });

    代码虽然很长,但每一个步骤都非常清晰。
    如果替换成lambda可能看上去会好看一点。

    代码写好后,尝试着测试一下,由于写了线程切换(android线程),所以只能在android中测试了。 如果希望在junit中测试,删掉所有的线程切换代码即可。

    跑一遍代码,发现cookie始终都是失效,并且会不断地重试。这是因为我们没对重试次数进行控制,这个次数控制在以上代码中实现非常简单, 加一个 次数技术就可以了,就不详细说了。

    现在我们看看是什么原因导致cookie一直失效呢,首先我们用的相同的cookieJar,应该不会不一样才对,并且重新登录后把新的accesstoken保存到了内存中。

    其实,之所以失效是因为对Obserable的工作机制没有理解透彻导致的。

    在创建一个Obserable的时候,参数的内容,固定的步骤就已经决定好了。
    即使在过程中参数发生了改变,retry的时候,还是使用原来的值去请求的。如何验证呢?同样用上篇博客中提到的HttpLoggingInterceptor 就可以验证是不是这样了。日志我就不贴了,验证结果就是参数并没有发生改变。

    所以,我们需要每次重试的时候,产生一个新的Obserable。这时候我们可以借助defer操作符来实现,每次调用都是一个新的Obserable。
    把请求接口的Obserable用Obserable.defer包裹一下即可。

    Observable.defer(new Func0>>>() {
                @Override
                public Observable>> call() {
                    return api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken);
                }
            })

    用lambda格式化一下

    Observable.defer(() -> api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken))

    是不是非常简单。
    用defer包裹之后再测试,就能够正常访问到接口的结果了。并且会在控制台输出一次cookie失效。说明成功调用了retry中的方法。

    到这里,基本上解决这个问题的步骤就差不多了,但是,我们还需要对这个重试操作进行封装一下,不用每次都复制这么长的代码,多累啊。

    最开始我想到的封装,就是封装两次,就把两个Func1封装起来就行了。
    最终调用方法如下:

    Observable.defer(() -> api.getCompanyInfo("GetCompanyInfo", Dic.key, Dic.accessToken))
                    .flatMap(new CookieDeal>())
                    .retryWhen(new ReLoginDeal().getFunc1(api))

    看上去还是封装的比较简单了。但是每次new CookieDeal的时候都要手动输入参数类型,这太不爽了,而且要写两个步骤,看上去一点也不优雅。

    最后我通过了解compose这个操作符的作用,对这个重试机制进行了终极封装。
    使用方法如下:

    .compose(ReLoginDeal.relogin())

    一句话,是不是不能再简单,关键是不用再手动打一个类型上去,测试的时候很方便。
    贴一下relogin()方法的代码吧:

    public static  Observable.Transformer, RequestResult> relogin() {
            return observable -> observable
                    .flatMap(tRequestResult -> {
                        if (tRequestResult.getErrcode() == -1) {
                            //重新登录。
                            System.out.println("cookie 失效");
                            return Observable.error(new RuntimeException("cookieError"));
                        }
                        return Observable.just(tRequestResult);
                    })
                    .retryWhen((Func1, Observable>) observable1 -> observable1.flatMap(new Func1>() {
                        @Override
                        public Observable call(Throwable throwable) {
                            if (throwable instanceof RuntimeException) {
                                    return RetrofitUtil.getUserApi().UserLogin("userLogin",
                                            MD5.hexdigest("key"),
                                            MyEncode.encode("fancy"),
                                            MyEncode.encode("123456"))
                                            .flatMap(stringRequestResult -> {
    
                                                Dic.accessToken = stringRequestResult.getAccesstoken();
                                                return Observable.just(1);
                                            });
                            }
                            return Observable.error(new IllegalArgumentException("没救了"));
                        }
                    }));
        }

    这个重试机制中登录的用户名密码是写死的,可以通过参数传递做成一个动态的。

    基本上封装就结束了,细节部分再自行优化一下就ok啦。

    对本文内容有任何疑问欢迎加群讨论:283272067

    你可能感兴趣的:(Android开发)