FluxJava: 给 Java 使用的 Flux 库

为何选择 Flux

设计上遇到的问题

最初在接触 Flux 时就有一种惊艳的感觉,长久以来在设计上所出现的困扰似乎出现了曙光。在 Flux 还没有出现之前,MVx 系列 (MVC、MVP、MVVM) 的 Design Pattern 就一直引领风潮。这类型的 Design Pattern 成功地解决了特定的问题,但却也形成了某些尾大不掉的隐忧。在画面不多、显示信息单纯的应用程序中问题不容易显现,但随着程序复杂度的升高,设计上所隐含的矛盾也不住地增强。

MVx 系列的设计在概念上是一个画面对应一种数据类型,画面专责显示与处理该类型的数据。很直觉、也很有效地把功能区分成一组、一组的单元。水能载舟亦能覆舟,正所谓成也萧何、败也萧何。就是因为每一组 MVx 太过独立、区隔性太强,当出现整合式画面的需求时,会造成在设计上进退两难的抉择。

假设程序中有一个画面叫 Dashboard,需要整合客户、订单、存货的数据。试问,这时是要设计一个新的 Model 纳入所有数据?还是打破规则让一个 View 对应多个 Model?

有人也许会问:这是问题吗?

如果只是期望程序能够运行,那的确算不上是个问题。但是如果要考虑到源代码的可维护性,就必须要维持在设计上的一致性,这点在程序愈复杂的情况下愈显重要。否则就不需要搞什么 Design Pattern,就随性而为、让一切都归于浑沌就好了。

再举另一个例子,假设要开发的是线上购物的订单画面,下单时要提供客户数据、订单数据、刷卡数据。依据之前的原则,所有的数据都会被设计纳在一个单一的 Model 内。当某一天高层突然下指令要把购物流程改成 Wizard 的方式,每个步骤各自独立成一个画面。试问在这样的情况下,开发新画面时是让 Model 拆解成多个?还是维持原本的样子?

如果要维持原本的样子,由于每一组的 MVx 都是独立的,如何传递 Model?谁要负责控制传递的顺序?又该如何保留 Model 的状态?好吧!那就拆开...

拆开之后,问题似乎解决了,但此时高层又说了,这个程序要跨平台,所以二种类型的画面都要有...

Flux 所提供的效果

Flux 的架构则是打破这层胶着的状态,在其单向数据流的原则之下,View 只要管显示数据,不管数据的来源是一个还是多个。而被通知数据有异动时,也是依循相同的方式来获取数据,刷新画面。至于要如何异动数据与 View 无关,只要把异动的信息传出去,接着就像战机上的飞弹一样可以射后不理。

在这样的设计之下,以之前 Dashboard 的例子,不管是单一的画面负责显示所有的数据,还是画面上分割成许多不同的组件来分别显示特定的数据,都不会有设计上的违和感。而另一个例子同样也适用,无关后端的数据规划方式,View 只要专注在选墿合适的数据来源、考量如何显示数据上即可。

Flux 只能用在有 UI 的情境之下?不尽然,并不是只有人才会输入或需要取得回应。在有明确的边界之状况下,像是网络或是因设计的考量所形成逻辑上的 Layer,这种可以用来把数据供给端及接收端做有效的分离,以便进行分工、测试等等作业的架构,都可以考虑套用 Flux 的概念。

如何实现

俗话说得好,知易行难。了解 Flux 的运作过程是一回事,但要把这些过程落实到设计之中、形成源代码又是另外一回事。Facebook 并没有为 Java 的开发环境开发一套符合 Flux 的库,而 Java 的环境相较于 JavaScript 又更加地多元化,加大了使用上的不确定性。为了避免在开发上每次都要反覆进行类似的工作,于是就依据过去的工作经验,利用抽象化的手法及自动生成的概念,实现了一个 Framework,让想要在 Java 的项目中使用 Flux 的人可以轻易的上手。

接下来会针对这个 Framework 做个简单的说明。

取得 Binary

最新版本的 Jar 档可以在 Github 的 Release 页面中下载。

配置

如果是使用 Gradle 来建构程序,则所下载到的档案可以送到 build.gradle 配置引用的文件夹下。如果是 Android 的项目,则是放到 libs 的文件夹下即可。

在项目中有使用 fluxjava-rx 时,应该也会需要在 build.gradle 中增加以下的内容:

dependencies {
    ...
    compile "io.reactivex:rxjava:1.2.+”
}

在使用之前

在 Github 的 Repository 中,FluxJava 库源代码放在 fluxjava 的文件夹下,并且在 demo-eventbus 文件夹下搭配一个示范用的 Android 项目。这个示范的项目是一个只有单一 Activity 的简易 Todo 应用程序。在这个 App 中可以展示以下的功能:

  • 显示 Todo 清单
  • 在不同使用者间切换 Todo 清单
  • 新增 Todo
  • 关闭/重启 Todo

在这个示范项目中使用 greenrobotEventBus 来协助 Dispatcher 和 Store 发送信息。

如果想要与 RxJava 搭配使用,可以看一下 fluxjava-rx 文件夹,里面有 FluxJava 为 RxJava 所开发的 Addon。同时,有一个与之配对的示范项目在 demo-rx 文件夹下,是由 demo-eventbus 复制过来修改的。在这个示范项目中,原本的 EventBus 以 fluxjava-rx 所提供的 RxBus 取代。而基于 RxJava 1.x 库的 RxBus 所提供的功能和 EventBus 的功能相同。

如何使用

准备工作

  • Bus
    Dispatcher 和 Store 会呼叫 Bus 来传送信息。Bus 必须要实现 IFluxBus 的介面,实现时可以使用任何的 Bus 方案,像是:Otto、EventBus,或是自行开发的方案。如果有同时引用 fluxjava-rx,则可以直接使用 RxBus 来提供传送信息的功能。

  • Action
    Dispatcher 使用 Action 来通知 Store 要进行的工作。在 Action 中有二个属性,一个是 Type、一个是 Data。Type 用来让 Store 识别要对数据进行的动作,Data 则是该动作的附属信息。以示范的项目来说,当一个新的 Todo 从介面上被传进来,则新 Todo 的内容会被放在 Data 栏位中。

  • ActionHelper
    ActionHelper 协助 ActionCreator 决定产生何种 Action,并且协助 ActionCreator 将目前传进来的数据格式转成可被处理的格式。

  • Store
    Store 负责截收由 Dispatcher 所送出的 Action,并根据 Action 上的信息进行对应的数据处理。当数据处理完成,Store 会再送出一个数据异动的事件,让事件的接收者可用以反应新的数据状态。

  • StoreMap
    StoreMap 是一个一对一的对照表,在 Framework 中使用这一个对照表来产生需要的 Store Instance。假设 Action 和 Store 的关系是一对一的,则 Action 的型别可以用来做为 Store 型别的键值,或是很单纯地使用一个数值来做为键值。像是在示范的项目中可以看到有二个常数在 Constants.java 中,分别是 DATA_USER 及 DATA_TODO,这二个常数各自会对应到一个 Store 的型别。因此,与 TodoAction 配对的 TodoStore 就会被产生来负责处理与 Todo 相关的数据要求,而 User 也是套用一样的逻辑。

初始化程序

在 FluxJava 中,FluxContext 是用来做为整个程序开始的进入点。FluxContext 被设计成 Singleton,负责整合 Framework 中相关的组件,并且管理特定组件的 Instance。

FluxContext 的 Instance 可以由其内含的 Builder 来建立,示范的源代码如下:

FluxContext.getBuilder()
        .setBus(new Bus())
        .setActionHelper(new ActionHelper())
        .setStoreMap(storeMap)
        .build();

开始发送要求

在取得使用者透过 UI 组件所输入的数据后,接下来可以利用 ActionCreator 来推送 Action,ActionCreator 的 Instance 可经由 FluxContext 来取得。Framework 预设所提供的 ActionCreator 只有一项功能 sendRequest,呼叫的源代码要传入 Id 及使用者输入的数据。其中,Id 是用来决定要产生的 Action 型别。使用者输入的数据可以在呼叫 sendRequest 后,经由 ActionHelper 转成 Store 所需的格式。

以下为示范的源代码:

Todo todo = new Todo();

FluxContext.getInstance()
        .getActionCreator()
        .sendRequestAsync(TODO_ADD, todo);

sendRequest 有提供二种版本的实现,同步和非同步。非同步的版本会先建立一个新的 Thread 之后,在新的 Thread 中执行。如果需要特别管控 Thread 的使用或是想要使用 Thread Pool,则可以呼叫同步的版本来达到目的。

进行数据处理

要进行数据处理需在 Store 中拦截指定的 Action,拦截的方法会依据所使用的 Bus 方案而不同。以示范项目的例子来说,要在 Store 中新增一个搭配特定 Annotation 的方法。相关的程序范例如下:

@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onAction(final TodoAction inAction) {
    switch (inAction.getType()) {
        case TODO_LOAD:
            ...
            super.emitChange(new ListChangeEvent());
            break;
        case TODO_ADD:
            ...
            super.emitChange(new ListChangeEvent());
            break;
        case TODO_CLOSE:
            ...
            super.emitChange(new ItemChangeEvent(i));
            break;
    }
}

如果是使用 fluxjava-rx,则 Store 可以继承自 RxStore,此时只要改写 RxStore 中的 onAction 方法即可。相关的程序范例如下:

@Override
protected <TAction extends IFluxAction> void onAction(final TAction inAction) {
    final TodoAction action = (TodoAction)inAction;

    switch (action.getType()) {
        case TODO_LOAD:
            ...
            super.emitChange(new ListChangeEvent());
            break;
        case TODO_ADD:
            ...
            super.emitChange(new ListChangeEvent());
            break;
        case TODO_CLOSE:
            ...
            super.emitChange(new ItemChangeEvent(i));
            break;
    }
}

反应数据异动

跟 Store 一样,UI 组件要依据使用的 Bus 方案来接收由 Store 所发出的数据异动事件。在 EventBus 的例子中:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(final TodoStore.ListChangeEvent inEvent) {
    super.notifyDataSetChanged();
}

在 RxBus 的例子中:

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

推荐阅读更多精彩内容