Google Agera 从入门到放弃

作者: rain 分类: 移动 发布时间: 2016-05-06 00:33 6 条评论

概述

Google Agera 是两周前 Google 开发团队开源出来的另外一个响应式编程(Reactive programming)框架。根据 Github 上的讨论结果,笔者猜测这个框架是这样形成的: Google Play Movie 是 谷歌 Android 上的一个浏览电影、购买电影、观看电影的 App,而谷歌的开发人员在开发 Google Play Movie 的时候(这个时候 RxJava 应该还没有发布呢?),根据响应式编程的思想来实现了一些功能,在后面的迭代开发中,把响应式编程相关的代码提炼了一个框架。而现在这帮人看到目前响应式编程思想这么火爆,尤其是 RxJava 最近两年被很多 Java 开发者和 Android 开发者追捧,于是他们说,“我们在 Play movie app 中使用的这个东西也可以提炼为一个专业用于 Android 平台的响应式编程框架,然后发布出去,让大家看看如何把!”。于是乎,Agera 就横空出世了。

既然是响应式编程框架,其核心思想肯定和 RxJava 也是类似的。如果你的 RxJava 还不熟悉,建议先看看前面的 Intro To RxJava 系列教程

Agera 中核心也是关于事件流(数据流)的处理,可以转换数据、可以指定数据操作函数在那个线程执行。

而 Agera 和 RxJava 不一样的地方在于, Agera 提供了一个新的事件响应和数据请求的模型,被称之为 “Push event, pull data”。也就是一个事件发生了,会通过回调来主动告诉你,你关系的事件发生了。然后你需要主动的去获取数据,根据获取到的数据做一些操作。而 RxJava 在事件发生的时候,已经带有数据了。为了支持 Push event pull data 模型, Agera 中有个新的概念 — Repository。 Repository 翻译过来也就是数据仓库,是用来提供数据的,当你收到事件的时候, 通过 Repository 来获取需要的数据。 这样做的好处是,把事件分发和数据处理的逻辑给分开了,事件分发做的事情比较少,事件分发就比较高效,当你收到事件后,根据当前的状态如果发现这个时候,数据已经无效了,则你根本不用请求数据,这样数据也就不用去处理和转换了。这样可以避免无用的数据计算,而有用的数据计算也可以在你需要的时候才去计算。

同样,在多线程环境下,使用 push event pull data 模型,可能当你收到事件通知的时候,你只能获取到最新的数据,无法去获取历史数据了。设计就是这样考虑的,应为在 Android 开发中大部分的数据都是用来更新 UI 的,这个时候你根本不需要旧的数据了,只要最新的就够了。

这样一个标准的 Agera 风格的应用包含如下几个基本组成部分:
– 在相关的事件源(Observable)上注册一个事件回调接口(updatable)
– 如果必要,手工的调用 updatable 的 回调接口,来触发一次事件用来初始化相关的内容
– 等待 事件源触发事件然后回调你的 updatable 接口,当你收到 回调的时候,从数据仓库中获取最新的数据来更新您的状态
– 当事件监听不再需要的时候(Activity 已经结束了),取消事件回调接口的监听。不然的话会导致内存泄露。

上面就是 Agera 的基本概念。下面来看看具体如何使用,也就是 Show me the code !

核心接口

Agera 的三个基本接口:

Observable.java

Updatable.java

Repository.java

上面这个 Repository 接口呢,同时继承了2个接口并没有添加新的功能。 一个是 Observable接口另外一个就是下面的提供数据的 Supplier 接口:

Supplier.java

上面就是 Agera 的核心接口。上面每个接口都有详细的说明,这里就不再叙述了。

需要注意的是,一个 observable(下家) 可能会监听另外一个 observable (称之为上家),根据上家 observable 数据的变化来更新自己事件。这样的话,这个 下家 observable 同时还是一个 updatable 用来接受上家的回到事件。

UI 生命周期

上面这种事件流的特性非常适合用于有 UI 生命周期的情况。UI元素可以为 一个 Activity、Fragment 或者 View。而 活动的状态可以根据 Android 的生命周期来判定。例如 在 Activity 的 onStart 中注册 updatable 然后在 onStop 中取消注册;或者使用 onResume 和 onPause;如果是 View 的话可以分别使用 onAttachedToWindow 和 onDetachedFromWindow 。这样 UI 元素可以实现 updatable 接口,或者持有一个 updatable 对象来获取事件更新的回调,然后通过repository 来获取数据更新 UI.

这样的话,UI 生命周期开始的时候,就注册了对应的 updatable 来接收事件更新 UI,注册 updatable 会激活所有的相关联的observable,这样 observable 激活后开始发射事件;当 UI 生命周期结束的时候,就取消注册,所有相关联的 observable 就变为非活动状态了,当 UI 元素被摧毁的时候, observable 也就没有引用 UI 元素了,可以避免内存泄露。

多线程

Agera 主张显式的使用线程。并使用 Looper 来帮助定义如下的线程约束。

对于内部的生命周期处理,每个 observable 都和一个 worker Looper 关联,这个 Looper 来自于这个 observable 被创建的线程。 Observable 在这个 worker Looper 线程中被激活和取消激活。如果这个 observable 还监听上家 observable 的事件的话,内部的 updatable 会从这个 worker Looper 中注册到上家 observable。

一个 updatable 必须从一个 Looper thread 来注册到 observable。这个线程可以不是 observable 的 worker Looper 线程。observable 会使用注册 updatable 的Looper 线程来通知 updatable(调用 Updatable.update())。

在任意线程都可以取消注册 updatable 。但是为了避免分发事件和取消注册事件的并发竞争问题,建议在注册 updatable 的线程中取消注册。

开发者需要确保 Observable 或者注册的 updatable 的 Looper 线程一直是运行的。由于 dead Looper(已经结束的 Looper)导致的异常和内存泄露问题应该有开发者自己负责。

虽然上面说的这么严重,但是在实际使用中,大部分情况下你只需要使用 main Looper 即可,而不需要使用其他 looper,而 main Looper 总是激活的,所以你无需关系 Looper 的活跃性问题。

Repositories

Repository 是一个提供数据的 observable ,所以在很多时候,使用 Agera 你都要和 Repository打交道。 Repository 可以是简单的只提供一个静态值的对象,比如返回一个服务器网址;也可以是一个动态的提供一个数据的对象,比如返回当前的手机电量。 而还有复杂的 Repository,复杂的 Repository 可以和其他 Repository 相互交互。并且多个 Repository 可以串联起来形成对数据处理的强大能力,就像 RxJava 中的操作函数一样。

而大部分实际开发过程总,我们都需要使用到复杂的 repository ,所以为了简化码农们的工作,Agera提供了一个 repository compiler 来帮助构造复杂的 repository。

repository compiler

复杂的 repository 可以通过一个 Java 表达式来创建。这个表达式包含下面一些按顺序排好的组成部分:

  1. Repositories.repositoryWithInitialValue(…);
  2. repository 的事件来源 – .observe(…);
  3. 事件响应的频度 – .onUpdatesPer(…) or .onUpdatesPerLoop();
  4. 数据处理的流程 – .getFrom(…), .mergeIn(…), .transform(…), etc.;
  5. 一些其他的配置项 – .notifyIf(…), .onDeactivation(…), etc.;
  6. 最后调用 .compile(). 来生成这个 复杂的 Repository

(注意最后必须调用 .compile() 来生成这个 Repository)
如果生成的 repository 被激活的话,这个 repository 会内部注册一个 updatable 到 他的事件来源,并开始数据处理流程来计算获取第一次需要使用到的数据。如果从事件来源收到回到事件更新的话,数据处理流程会再次执行来计算获取最新的数据。在第一次计算完成之前,如果你调用 repository 的 get 函数来获取数据,会返回第一步指定的初始化的值。如果 repository 的值发生了变化,则注册的 updatable 就会收到回调函数。当 repository 变成非活跃状态了,内部的监听事件源的 updatable 就也会取消注册,数据处理流程停止执行。这个时候调用 get 来获取到的值可能就不是最新的了。当被重新激活后,其返回的值会再次更新。

而创建 Repository 的这 6 个步骤呢,有可以看作是 6 中状态。为了方便编码(使用现代 IDE 的代码提示功能),每个状态都定义了一个对应的接口,这些接口呢在 RepositoryCompilerStates 这个接口内定义。RepositoryCompilerStates 中有一些嵌套的状态接口。

RepositoryCompilerStates.java

何时何地干什么

RepositoryCompilerStates 接口中定义的各个创建 Repository 的状态,明确的定义了 Repository 何时响应时间源,在那个线程中执行响应,以及如何处理数据。
看了 RepositoryCompilerStates 中定义的函数后,通过一个示例,再结合 RxJava 的理论,就可以发现其实还是很容易理解的:

完整的示例查看这里: NotesActivity.java

上面实现的功能是从一个图片网址中获取一个图片,这个图片网址可以带有参数用来告诉服务器返回多大尺寸的图片。比如 Github 获取用户头像的网址 https://avatars1.githubusercontent.com/u/587927?v=3&s=460,网址中可以用参数 s 指定需要返回的图片尺寸,如果你只想显示一个小头像,可以指定 s=128.

repositoryWithInitialValue 这里指定的初始值为空(也就是没有初始值);随后调用函数 observe,参数也是空,说明这个 Repository 没有响应其他事件源;onUpdatesPerLoop 指定了更新的频度;getFrom 函数指定了从 sizeSupplier 总获取数据流处理过程中的第一个数据,这个 sizeSupplier 继承了 Supplier 接口返回手机屏幕宽度和高度的最大的那个尺寸;然后 goTo 函数指定了后续的数据处理流程在 networkExecutor (非 UI 线程)线程中执行,后续有三个操作;mergeIn 是把 sizeSupplier 提供的屏幕尺寸和 backgroundUrlSupplier 提供图片的网址一起合并为(通过 sizedUrlMerger)一个完整的带参数的url地址;然后呢把这个url地址通过 transform 函数转换为一个 HttpRequest 对象(这里面使用到了 agera 网络支持库提供的封装);然后 attemptTransform 用这个 HttpRequest 对象来做请求服务器的操作,httpFunction 函数返回的是一个 UrlConnectionHttpFunction 转换函数,把 HttpRequest 转换为 网络请求的返回数据;注意后面跟随一个 .orSkip() 函数,如果这个请求网络的操作失败了,则跳过后面的情况 (比如 手机没网了、服务器宕机了 都会导致请求图片失败),这样请求图片失败了,就无需执行后面的流程了,也无需通知 UI 更新图片了。如果请求图片返回结果了, 则 通过 goTo 进入另外一个 calculationExecutor 线程来处理返回的结果(这里只是为了演示 goTo 的用户,你直接在 networkExecutor 中执行转换也是可以的,但是这里使用 calculationExecutor 来处理网络返回的数据是非常好的一种分担任务的方式,大家也经常忽略这个额外的线程,这样做的好处是 请求网络在网络请求线程中处理,处理网络返回的数据在数据处理线程中处理,如果一个请求返回的数据需要长时间加工处理,使用一个额外的数据处理线程则可以避免网络请求被阻塞,这样你可以处理前一个网络请求返回的结果,而继续在网络请求线程中请求下一个需要的网络数据,有效的降低应用加载数据的时间。);如果没有 Updatable 监听这个 Repository了,就通过 onDeactivation(SEND_INTERRUPT)来终端数据处理流程。比如应用退出了,再请求加工图片也没用了。最后的 compile() 来生成这个 Repository。生成的 Repository 对象保存到 backgroundRepository 变量:

Repository 生成以后呢,如果没有 updatable 注册到其上面,则上面的数据处理流程是不会执行的,当你需要数据的时候,比如在 Activity 的 onResume 函数中,把 updatable 注册到 Repository 上即可触发数据更新:

当 Repository 数据变化的时候呢,会回到你的 update 函数,在这里可以通过 Repository 来获取数据,根据数据来更新 UI:

如果你使用上面的方式来更新图片到一个 ImageView 上,则说明你还没有用 Rx 的思想来思考问题。在 Agera 中应该这样使用:

ifSucceededSendTo 顾名思义,如果成功的获取到了图片,把这个 图片发送给 ImageViewBitmapReceiver。然后就会调用 ImageViewBitmapReceiver 的 accept 函数,然后在这个函数里面设置 ImageView 的图片。 是不是又绕了一大圈?!

上面的 Receiver 接口是一个数据接收容器:

数据处理流程的高级用法

在上面示例中,可以看到数据处理流程输入的初始值为 屏幕尺寸(Integer),经过一系列的处理返回一个 Bitmap 对象。而数据处理流程中,数据类型可以随意转换。可以把任何值转换为你最终想要的对象。这种情况在 Android 开发中(或者其他软件开发中)是非常常见的。比如:
1. 从服务器下载一堆二进制数据
2. 把二进制数据解析为一些对象集合
3. 从这些对象集合中提取每一个单独的对象
4. 然后把每个单独的对象转换为 Android 中可用的对象
5. 还可以过滤到不符合结果的对象
6. 最后把处理过的对象数据显示到 UI 中

为了是代码看起来更直观,码农可以选择把每个步骤的处理过程封装为一个 Function 对象:

上面代码示例中的 HTTP_RESPONSE_TO_BITMAP 就是一个 Function,其功能就是把 HttpResponse 转换为一个 Bitmap。

比如在一个数据流转换过程中,可能需要下面多个 Function 转换:

如果一个一个的应用每个转换,可能会让代码看起来有点杂乱无章。为了让代码更加简洁呢, Agera 提供了一个可以 Functions 助手类来把多个连续的 Function 转换为一个 Function,其使用方式和创建 Repository 类似。

这样的话,只需要使用 urlToUiModels 即可。但是你可能会问, 为啥一开始要使用五个 Function 来一步一步的转换类型呢, 我完全可以在一个 Function中完成这些转换嘛?

这样做当然是可以的,但是这样做你又没有用 Rx 的思想来解决问题。 在 Agera 中,前面每个小的 Function 都是一个独立的转换,通过 Functions 提供的功能可以随意组合这些独立的 Function 来实现强大的功能,就像排列组合一样,导致代码可重用行提高,可阅读行也提高。 当然了,你会说, 我上面把每个转换封装为一个函数,在一个 Function 中调用需要的一组函数,直接生成需要的结果也可以排列组合里面的函数哦! 这个问题就待你在实际使用的过程中去体会吧。

同样 Function 创建也有各种状态,和创建Repository 一样,有一个 FunctionCompilerStates 接口里面定义了各种子接口。这个具体 API 比较简单, 这里就不介绍了。只是一个辅助功能并不是 Agera的必备组成部分。

Reservoir 和并行计算

Reservoir 是 Agera 中另外一个接口:

这个玩意既是一个 Receiver 也是一个 Repository。

这玩意是干啥用的呢?

前面已经说过了,Agera 是 push event pull data 模型,在多线程环境中,一个 updatable 可能无法看到所有的历史数据。 Agera 设计就是这样的,反正大部分 UI 更新数据只要最新的就够了。但是个别情况下 客户端可能还是需要知道数据变化的历史的,这个时候你就需要使用 Reservoir 了。

Reservoir 是一个响应式编程风格的 Queue。 通过 Receiver 接口可以把数据保存到 Reservoir 中。而通过 Repository (Supplier )接口则可以获取里面保存的数据。访问一个 reservoir 是线程安全的,所以同一时刻只有一个线程能访问 Reservoir 中的数据,返回值为一个 Result ,如果没有数据的话,返回一个 Result.absent() 可以避免 Java 中的 null 检测。

这样,Reservoir 就非常适合一种情况:作为一些数据需要逐条处理的数据源。当有处理请求的时候,就发送给 Reservoir,然后有数据后就通知处理数据的一方来处理里面的数据。例如: 把数据保存到数据库中这个场景。

客户端可能在和服务器同步数据,而用户也可能在同时添加、编辑和删除数据。如果保存数据需要一些时间的话,则可以把用户的每次操作都丢到这个 Reservoir 中,然后有个保存数据的 Repository 监听这个 Reservoir。当 Reservoir 中有数据的时候,就触发 Repository 来执行保存数据的操作。这样的话,保存数据的 Repository 就可以看到用户操作数据了一系列指令了,然后逐条保存。

这里是一个类似的示例:
NotesStore.java

在 notesStore 函数中,使用 writeRequestReservoir 来接收用户保存、修改、和删除数据的命令,收到命令后会触发 writeReaction 这个 Repository 执行数据处理流程,然后会调用 writeOperation 来最终执行每个用户的命令,是保存、修改还是删除数据。

自定义 observable

和 RxJava 一样, Agera也提供了一些辅助功能来简化自定义 Observable。

代理 Observable (Proxy observables)

代理模式在 Java 中常用的模式之一。为了简化创建代理 Observable, Agera 提供了一个 observables 类,里面包含了一些常用的功能:
– compositeObservable 用来把多个 Observable 组合一起
– conditionalObservable 只有当满足条件的时候才把 源 Observable 的事件分发出去
– perMillisecondObservable 和 perLoopObservable 可以限制源 Observable 事件分发的频率。

BaseObservable

如果你看了 observables 类的源代码后, 发现上面这几个代理 Observable 类只是继承 BaseObservable 并需要简单的几行代码既可以实现了。 BaseObservable 实现了 Observable 的绝大部分逻辑,所以继承 BaseObservable 来自定义 Observable 是非常容易的。自定义的 Observable 只需要在事件发生的时候调用 dispatchUpdate 函数来分发事件(任意线程都可以哦)即可。例如下面是一个点击 View 的时候分发事件的实现方式:

通过覆盖函数 bservableActivated() 和 observableDeactivated(),自定义 Observable还可以监听到 Observable 的生命周期。这两个函数在 BaseObservable 的 worker Looper 线程中调用(也就是 BaseObservable 对象被创建的那个线程)。

UpdateDispatcher

如果无法继承 BaseObservable,则可以只实现 Observable 接口然后使用 UpdateDispatcher 来管理事件状态。

在自定义的 Observable 对象中通过 Observables.updateDispatcher() 函数来获取一个 UpdateDispatcher 实例,然后把相关的 Observable 注册和取消注册调用转发到 UpdateDispatcher 实例即可。分发事件只需要调用 UpdateDispatcher.update() 函数即可。

下一节会有一个示例。

通过前面的介绍,可以看到 Agera 是一个专门为 Android 平台开发的一个轻量级的响应式编程框架。 如果是一个新的 Android app 从头开始开发,使用 Agera 相对好用。但是如何现有的应用该如何使用 Agera 呢?

在遗留代码中使用 Agera

升级遗留的观察者模式

观察者模式有各种各样的实现,并非每种实现都可以很容易的迁移到 Agera 风格的实现。

下面示例演示了如何把传统的 Listener 接口来实现 Observable 功能的,由于下面示例中的 MyListenable 已经继承了其他的类,所以只能使用 UpdateDispatcher 来实现其功能:

把同步请求数据的操作包装为 Repository

比如传统的应用,请求网络数据然后更新UI,则可以把这个同步请求网络的函数封装为一个 Repository,然后在后台线程中执行请求网络的操作即可:

上面示例中,通过 Supplier来封装请求网络的操作,然后 创建一个 Repository 在 networkingExecutor 线程中执行 thenGetFrom(networkCall)。当数据返回后回调 UI ,UI再从 Repository 中拿到最新的数据显示 UI。

上面只是封装了一个网络请求的调用,如果你的应用中有很多网络请求,只是请求的参数不一样,则可以使用一个可变的 repository 作为存储参数的数据源。为了让 Repository 创建后即可执行第一个请求,则可以用一个 Result 来包装请求的参数并把初始值设置为absent(),例如 下面的 requestVariable:

然后不再使用 supplier 来封装这个网络请求,而是使用可以带有动态参数的 function 来封装:

这样可以用 request 参数来修改请求的网址参数。

修改后的 Repository 是这样的:

这里的 Repository 监听 requestVariable 参数的改变: observe(requestVariable)

把异步请求数据的操作包装为 Repository

现在很多的 Android 库都直接支持异步请求数据了。但是这些库异步调用的问题是无法保证回调函数在哪个线程调用的,例如:

上面的回调函数 onResponse 可以在任何线程中调用。

下面是一种封装异步调用为 Repository的方式:

使用通用的方式也可以修改为支持请求参数的版本:

总结

可以看到 Agera 比较简单,并且是一个专业为 Android 平台打造的响应式编程框架。但是如果你熟悉了 RxJava 你会发现其用起来会有点不舒服,特别是还要主动的获取数据略感不爽(虽然提高了事件分发的效率)。 虽然现在 Agera已经有了对 SharedPreference、BroadcastReceiver、SQLite 数据库、网络请求甚至还支持把 Repository 数据直接映射到 RecyclerView adapter 上的支持。但是使用起来还是没有 RxJava + Retrofit + RxBinding 等各种 Rx Android 第三方库用起来更加得心应手。

如果你开始一个新项目,并且感觉 RxJava 以及各种支持库 略显笨重,则可以试试 Agera。 反正我还是继续使用 RxJava 了!

本文出自 云在千峰,转载时请注明出处及相应链接。

本文永久链接: http://blog.chengyunfeng.com/?p=984

Ɣ回顶部