RxJava处理嵌套请求

互联网应用开发中由于请求网络数据频繁,往往后面一个请求的参数是前面一个请求的结果,于是经常需要在前面一个请求的响应中去发送第二个请求,从而造成“请求嵌套”的问题。如果层次比较多,代码可读性和效率都是问题。本文首先从感性上介绍下RxJava,然后讲解如何通过RxJava中的flatMap操作符来处理“嵌套请求”的问题

内容提要

  • RxJava简单介绍
  • 嵌套请求举例
  • 运用flatMap
  • map和flatMap
  • RxJava与Retrofit配合解决嵌套请求

博客园对应原文

RxJava简单介绍

这里并不打算详细介绍RxJava的用法和原理,这方面的文章已经很多了。这里仅仅阐述本人对于RxJava的感性上的理解。先上一个图:


Rxjava overview

我们都知道RxJava是基于观察者模式的,但是和传统的观察者模式又有很大的区别。传统的观察者模式更注重订阅和发布这个动作,而RxJava的重点在于数据的“流动”。
如果我们把RxJava中的Observable看做一个盒子,那么Observable就是把数据或者事件给装进了这个易于拿取的盒子里面,让订阅者(或者下一级别的盒子)可以拿到而处理。这样一来,原来静态的数据/事件就被流动起来了。

我们知道人类会在河流中建设大坝,其实我们可以把RxJava中的filter/map/merge等Oberservable操作符看做成数据流中的大坝,经过这个操作符的操作后,大坝数据流被过滤被合并被处理,从而灵活的对数据的流动进行管制,让最终的使用者灵活的拿到。

以上就是我对RxJava的理解,深入的用法和原理大家请自行看网上的文章。

嵌套请求举例

这里开始进入正题,开始举一个嵌套请求的例子。
比如我们下列接口:

  1. api/students/getAll (传入班级的id获得班级的学生数组,返回值是list<Student>)
  2. api/courses/getAll (传入Student的id获得这个学生所上的课程,返回值是List<Course>)

我们最终的目的是要打印班上所有同学分别所上的课程(大学,同班级每个学生选上的课不一样),按照传统Volley的做法,代码大概是这样子(Volley已经被封装过)

private void getAllStudents(String id) {
        BaseRequest baseRequest = new BaseRequest();
        baseRequest.setClassId(id);
        String url = AppConfig.SERVER_URL + "api/students/getAll";

        final GsonRequest request = new GsonRequest<>(url, baseRequest, Response.class, new Response.Listener<Response>() {
            @Override
            public void onResponse(Response response) {
                if (response.getStatus() > 0) {
                    List<Student> studentList = response.getData();
                    for (Student student : studentList) {

                    }
                } else {
                    //error
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                //error
            }
        });
        MyVolley.startRequest(request);
    }

    private void getAllCourses(String id) {
        BaseRequest baseRequest = new BaseRequest();
        baseRequest.setStudentId(id);
        String url = AppConfig.SERVER_URL + "api/courses/getAll";

        final GsonRequest request = new GsonRequest<>(url, baseRequest, Response.class, new Response.Listener<Response>() {
            @Override
            public void onResponse(Response response) {
                if (response.getStatus() > 0) {
                    List<Course> courseList = response.getData();
                    for (Course course : courseList) {
                        //use
                    }
                } else {
                    //error
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                //error
            }
        });
        MyVolley.startRequest(request);
    }

显然第一个请求的响应中获得的数据是一个List,正对每一个List中的item要再次发送第二个请求,在第二个请求中获得最终的结果。这就是一个嵌套请求。这会有两个问题:

  • 目前来看并不复杂,如果嵌套层次多了,会造成代码越来越混乱
  • 写出来的重复代码太多

运用flatMap

现在我们可以放出RxJava大法了,flatMap是一个Observable的操作符,接受一个Func1闭包,这个闭包的第一个函数是待操作的上一个数据流中的数据类型,第二个是这个flatMap操作完成后返回的数据类型的被封装的Observable。说白了就是讲一个多级数列“拍扁”成了一个一级数列。
按照上面的列子,flatMap将接受student后然后获取course的这个二维过程给线性化了,变成了一个可观测的连续的数据流。
于是代码是:

ConnectionBase.getApiService2()
                .getStudents(101)
                .flatMap(new Func1<Student, Observable<Course>>() {
                    @Override
                    public Observable<Course> call(Student student) {
                        return ConnectionBase.getApiService2().getAllCourse(student.getId());
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Course>() {
                    @Override
                    public void call(Course course) {
                        //use the Course
                    }
                });

是不是代码简洁的让你看不懂了?别急,这里面的getStutent和getAllCourse是ConnectionBase.getApiService2()的两个方法,他集成了Retrofit2用来将请求的网络数据转化成Observable,最后一节将介绍,这里先不关注。
我们所要关注的是以上代码的流程。
首先getStudent传入了班级id(101)返回了Observable<Student>,然后链式调用flatMap操作符对这个Observable<Student>进行变换处理,针对每一个发射出来的Student进行再次请求 ConnectionBase.getApiService2().getAllCourse从而返回Observable<Course>,最后对这个 ConnectionBase.getApiService2().getAllCourse进行订阅,即subscribe方法,再Action1这个闭包的回调中使用course。

flatMap的作用就是对传入的对象进行处理,返回下一级所要的对象的Observable包装。

FuncX和ActionX的区别。FuncX包装的是有返回值的方法,用于Observable的变换、组合等等;ActionX用于包装无返回值的方法,用于subscribe方法的闭包参数。Func1有两个入参,前者是原始的参数类型,后者是返回值类型;而Action1只有一个入参,就是传入的被消费的数据类型。

subscribeOn(Schedulers.io()).observeOn(AndroidScheduler.mainThread())是最常用的方式,后台读取数据,主线程更新界面。subScribeOn指在哪个线程发射数据,observeOn是指在哪里消费数据。由于最终的Course要刷新界面,必须要在主线程上调用更新view的方法,所以observeOn(AndroidScheduler.mainThread())是至关重要的。

map和flatMap

运用flatMap的地方也是可以用map的,但是是有区别的。先看下map操作符的用法:

ConnectionBase.getApiService2()
                .getStudents(101)
                .map(new Func1<Student>, Course>() {
                    @Override
                    public Course call(Student student) {
                        return conventStudentToCourse();// has problem
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Course>() {
                    @Override
                    public void call(Course course) {
                        //use the Course
                    }
                });

可以看到map也是接受一个Func1闭包,但是这个闭包的第二个参数即返回值参数类型并不是一个被包装的Observable,而是实际的原始类型,由于call的返回值是Course,所以conventStudentToCourse这里就不能用Retrofit2的方式返回一个Observable了。

所以这里是有一个问题的,对于这种嵌套的网络请求,由于接到上端数据流到处理后将结果数据放入下端数据流是一个异步的过程,而conventStudentToCourse这种直接将Student转化为Course是没法做到异步的,因为没有回调方法。那么这种情况,最好还是用flatMap并通过retrofit的方式来获取Observable。要知道,Rxjava的一个精髓就是“异步”。

那么到底map和flatMap有什么区别,或者说什么时候使用map什么时候使用flatMap呢?
flatMap() 和 map() 有一个相同点:它也是把传入的参数转化之后返回另一个对象。但需要注意,和 map() 不同的是, flatMap() 中返回的是个 Observable 对象,并且这个 Observable 对象并不是被直接发送到了 Subscriber 的回调方法中。

首先,如果你需要将一个类型的对象经过处理(非异步)直接转化成下一个类型,推荐用map,否则的话就用flatMap。
其次,如果你需要在处理中加入容错的机制(特别是你自己封装基于RxJava的网络请求框架),推荐用flatMap。
比如将一个File[] jsonFile中每个File转换成String,用map的话代码为:

Observable.from(jsonFile).map(new Func1<File, String>() {
    @Override public String call(File file) {
        try {
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            // So Exception. What to do ?
        }
        return null; // Not good :(
    }
});

可以看到这里在出现错误的时候直接抛出异常,这样的处理其实并不好,特别如果你自己封装框架,这个异常不大好去抓取。

如果用flatMap,由于flatMap的闭包返回值是一个Observable,所以我们可以在这个闭包的call中通过Observable.create的方式来创建Observable,而要知道create方法是可以控制数据流下端的Subscriber的,即可以调用onNext/onCompete/onError方法。如果出现异常,我们直接调用subscribe.onError即可,封装框架也很好感知。代码大致如下:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(final File file) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override public void call(Subscriber<? super String> subscriber) {
                try {
                    String json = new Gson().toJson(new FileReader(file), Object.class);

                    subscriber.onNext(json);
                    subscriber.onCompleted();
                } catch (FileNotFoundException e) {
                    subscriber.onError(e);
                }
            }
        });
    }
});

map操作符通常也用于处理结构化的服务端响应数据,比如下列返回的JSON数据就是一段典型的响应数据

{
    "message":"操作成功",
    "status":1,
    "data":
    {
        "noVisitCount":0,
        "planCount":0,
        "visitedCount":0
    }
}

在map的闭包中,我们可以先判断status进行统一的出错或者正确(返回data的内容)处理,一般来说,data的内容都是处理成一个泛型

RxJava与Retrofit配合解决嵌套请求

这里该讨论Retrofit了。可以说Retrofit就是为了RxJava而生的。如果你的项目之前在网络请求框架用的是Volley或者自己封装Http请求和TCP/IP,而现在你看到了Retrofit这个框架后想使用起来,我可以负责任的跟你说,如果你的项目中没有使用RxJava的话,使用Retrofit和Volley是没有区别的!要用Retrofit的话,就最好或者说强烈建议也使用RxJava进行编程。

Retrofit有callback和Observable两种模式,前者就像传统的Volley一样,有successs和fail的回调方法,我们在success回调方法中处理结果;而Observable模式是将请求回来的数据由Retrofit框架自动的帮你加了一个盒子,即自动帮你装配成了含有这个数据的Observable,供你使用RxJava的操作符随意灵活的进行变换。

callback模式的Retrofit是这样建立的:

retrofit = new Retrofit.Builder()
                .baseUrl(SERVER_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

Observable模式是这样子建立的:

retrofit2 = new Retrofit.Builder()
                .baseUrl(SERVER_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

即addCallAdapterFactory这个方法在起作用,在RxJavaCallAdapterFactory的源码注释中可以看到这么一句话:

Response wrapped body (e.g., {@code Observable<Response<User>>}) calls {@code onNext} with a {@link Response} object for all HTTP responses and calls {@code onError} with {@link IOException} for network errors

即它将返回值body为包裹上了一层“Observable”

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • 我从去年开始使用 RxJava ,到现在一年多了。今年加入了 Flipboard 后,看到 Flipboard 的...
    Jason_andy阅读 5,451评论 7 62
  • 前言我从去年开始使用 RxJava ,到现在一年多了。今年加入了 Flipboard 后,看到 Flipboard...
    占导zqq阅读 9,158评论 6 151
  • 文章转自:http://gank.io/post/560e15be2dca930e00da1083作者:扔物线在正...
    xpengb阅读 7,017评论 9 73
  • 什么也没干,好像做了两道容易的LeetCode题,折腾了toree问题和sbt仓库的问题。再也不能这样,不教一日闲过。
    DataNerd阅读 195评论 0 0
  • 快乐不是因为有钱, 而是因为开心; 烦恼不是因为没钱, 而是因为自私。 这个世界的好多问题与金钱有关, 但是更多最...
    再凑热闹阅读 136评论 0 0