-->

Neater

Be A Warrior, Not A Worrier

RxJava as Event Bus

前不久 Square 宣布不在维护 Otto,令人欣喜的是,Android 里还可有 RxJava 能实现同样的功能。

棘手的问题是,如何把基于 Otto 和 Greenbot’s EventBus 的代码迁移到 Rx(有时称为 RxBus)。在讨论使用 RxJava 实现事件总线之前,先来说说事件总线。

什么是事件总线

很简单,连接两个不同事物的机制,可以有不同的生命周期,在不同的层级,等等。

Activity,Fragment,Service 还有 dialog 之间通讯想必是我们很大的痛点,比如:

  1. Service 中产生事件,需要在 Activity 中改变按钮的颜色
  2. Fragment需要 Activity显示一个 Dialog,根据用户之后的操作再改变 Fragment中的内容,在这3个源文件中进行回调的操作,光想一下就真够累的,如果再增加几个特性,工作量可想而知了。

事件总线的解决方案是:通过特定的事件总线连接两个类,并发送自定义的数据

在类 A 中产生事件

1
2
3
4
5
// EventBus
eventBus.post(new AnswerAvailableEvent(42));

// Otto
bus.post(new AnswerAvailableEvent(42));

在类 B 中注册并接收事件

1
2
3
4
5
6
7
8
9
10
11
12
// EventBus
eventBus.register(this); // e.g. in onCreate
...
public void onEvent(AnswerAvailableEvent event) {

};

// Otto
@Subscribe
public void answerAvailable(AnswerAvailableEvent event) {

}

Rx 登场

监听者、Rx、事件总线都使用了响应式模式,只不过 RxJava 做的更好,在我看来,主要是下面几个原因:

  1. 更容易追踪代码逻辑
  2. 更容易测试
  3. 简化线程调用
  4. 提供了多种事件流
  5. 易操作、易重用、易模块化

单事件流与多事件流的对比

理解了事件总线和 RxJava 之后,你是不是有自己写个轮子的冲动,我劝你还是打消这个念头吧,先看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyRxBus {

  private static MyRxBus instance;

  private PublishSubject<Object> subject = PublishSubject.create();

  public static MyRxBus instanceOf() {
    if (instance == null) {
      instance = new MyRxBus();
    }
    return instance;
  }

  /**
   * Pass any event down to event listeners.
   */
  public void setString(Object object) {
    subject.onNext(object);
  }

  /**
   * Subscribe to this Observable. On event, do something 
   * e.g. replace a fragment
   */
  public Observable<Object> getEvents() {
    return subject;
  }
}

还需要引入相应的事件,并进行相应的类型检查。

1
2
3
4
5
6
7
8
getEvents().subscribe(new Action1<Object>() {
  @Override
  public void call(Object o) {
    if(o instanceof AnswerAvailableEvent){
      // do something
    }
  }
});

上面代码不容易测试,没有做到模块,还引入了不必要的事件类,那么我们引入新的概念

模块化的响应式

刚才的代码不易测试,没有模块化,还引入了不必要的事件类,现在再来看看:

模块化响应式

模块化的响应式理念来自于 Futurice’s repo,说的是在 Activity 和 Fragment 里使用多种模型来处理业务数据和网络访问

比如,UserLocationModel 是提供物理位置的模型类,在 Activity 获得设备坐标然后发给相应的 Fragment

相应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserLocationModel {

  private PublishSubject<LatLng> subject = PublishSubject.create();

  public void setLocation(LatLng latLng) {
    subject.onNext(latLng);
  }

  public Observable<LatLng> getUserLocation() {
    return subject;
  }
}

很好的完成了模块化,将来也更容易进行扩展,将来还可以还细分:

看到这,你会发现也没有那么难嘛,这种模式已经在我的许多项目中进行了充分的测试,在这方面没有比 RxJava 做的更好的了。

小技巧

  • 尽可能使用小巧的模型类便于测试
  • 业务逻辑应该在模型中,而不是 Activities/Fragment/Dialogs
  • Keep the threading logic in the Activities/Fragments/Dialogs because unit testing is a bit harder (but not)
  • 使用 Dagger 或手工降低模块间的依赖

译自 https://medium.com/@diolor/rxjava-as-event-bus-the-right-way-10a36bdd49ba

RxJava 中的线程

最近有不少人开始使用 RxJava,但令人困扰的是:我们的业务逻辑链及其操作应该运行在哪个线程中。首先需要区分 .subsribeOn().observeOn():

  • .subsribeOn() 决定了 Observable 的操作所在的线程
  • .observeOn() 决定了在哪个线程接收 Observable 的数据
  • 需要注意的是,默认地,Observable 调用链运行在调用 .subscribe() 的线程中

例子

1. Main thread / .subscribe() thread

在 Android 的 Activity 中的 onCreate() 方法中运行下面的代码,将会在 UI 线程中

1
2
Observable.just(1,2,3)
  .subscribe();

如何过渡到 RxAndroid 1.0”

最近有许多人都在问我,操蛋的 RxAndroid 是怎么了?说真的,它确实是有点乱,这也是它最近被重构的主要原因,详细请看这里,主要是说:

我建议从头开始,改善它的模块化问题,提高可重用性和可组合性。

虽然已经完成了这个目标,但我们过渡到新版本时,出现了新的问题: 原来的 API 都跑哪去了,我该如何才能通过编译?

RxAndroid

AndroidSchedulers 的代码全部保留,仅仅改变了一些方法的签名。

改动

WidgetObservableViewObservable 经过完善后,移到了 RxBinding

LifecycleObservable 移到了 RxLifecycle,不过它被大量的重构了,你可能需要好查看一下它的更新日志。

还有 ContentObservable.fromSharedPreferencesChanges() 经过完善后,移到了 rx-preferences

孤立

ContentObservable 其余部分还没有人来继续这项开源项目。

移除

AppObservable 及其一系列绑定方法全部删除,主要是下面几个原因:

  1. 原本想自动取消订阅,但只在 Activity 和 Fragment 暂停时实现了,数据源最终也不会取消订阅
  2. 原来想防止 App 在暂停的时候收到通知,但在 HandlerScheduler 里出现了不容易察觉的逻辑问题
  3. 自动调用 observeOn(AndroidSchedulers.mainThread())

换句话说,它非但没有实现当初的设计目标,而且还过于保守,副作用的问题也让人不快。

使用它时,请注意:

  1. 通过 Subscription(或者使用 RxLifecycle)取消订阅
  2. 是否真的需要使用 observeOn(AndroidSchedulers.mainThread())

译自 http://blog.danlew.net/2015/09/01/how-to-upgrade-to-rxandroid-10/

RxJava 具有副作用的方法

RxJava 有许多方法可以把流中的数据项转化成需要的数据,它们是 RxJava 的核心,也是最吸引人的部分。与之对应的是,其它方法不更改流中的数据项,我称它们为副作用方法。

副作用方法

虽然副作用方法不影响流,但它们会在一些特定的事件中被调用,从而响应这些事件。比如:当一个被订阅的流产生错误的时候,可以 doOnError() 会被调用,这时把一些调试代码写在 doOnError() 里,就能知道到底发生了什么错误。

RxJava 在 Android 开发中的初次尝试(下)

这次我们来完成⎾RxJava 在 Android 开发中的初次尝试⏌的下半部分,在上次代码的基础上,用 RxJava 实现网络请求、多线程、AsyncTask。

在点击Submit按钮后,发起一次网络请求,可以用 Retrofit,对 RxJava 支持的很好,不过我不打算用他,我想用 RxJava 来模拟网络请求实现进度条的显示功能。

1
2
3
4
5
6
7
8
9
10
private void submit(InputValidation inputValidation) {
    request(inputValidation)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe(() -> enableWidgets(false))
            .doOnCompleted(() -> enableWidgets(true))
            .doOnError(throwable1 -> enableWidgets(true))
            .subscribe(progressBar::setProgress
            );
}I

不过要注意的是,网络等需要多线程的代码需要调用 subscribeOn(Schedulers.io()),不然会报 ANR 错误,还有要对界面进行操作需要调用 observeOn(AndroidSchedulers.mainThread())doOn*这一类方法会在更阶段进行回调,我把一些状态设置和显示的代码写在里面

1
2
3
4
5
6
7
8
9
10
11
12
private void enableWidgets(boolean enable) {

    userName.setEnabled(enable);
    password.setEnabled(enable);
    confirmedPassword.setEnabled(enable);

    int visible = enable ? View.INVISIBLE : View.VISIBLE;
    progressBar.setVisibility(visible);

    String text = enable ? "end submit" : "begin submit";
    Snackbar.make(submitButton, text, Snackbar.LENGTH_SHORT).show();
}

模拟网络请求的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Observable<Integer> request(InputValidation inputValidation) {
    return Observable.create(subscriber -> {
        try {
            for (int i = 1; i < 101; i++) {
                subscriber.onNext(i);
                Thread.currentThread().sleep(30);
            }
            subscriber.onCompleted();
        } catch (InterruptedException e) {
            e.printStackTrace();
            subscriber.onError(e);
        }
    });
}

把用户输入的代码移到 InputValidation.java 里,这里不贴代码了。最终效果:

submit

RxJava 在 Android 开发中的初次尝试(上)

接触 RxJava 有一段时间了,写的都是一些小打小闹的 Sample 程序,不如就在实际项目中一试伸手。

许多 App 都有注册功能,当没有输入或者输入不符合业务逻辑时,会有错误提示。好点的用户体验应该是,只有当所有输入符合业务逻辑时,才会让注册按钮变成可点击状态,代码如下:

蔡学鏮语录

蔡学镛是我非常喜欢的台湾架构师,著有《编程ING》《Java夜未眠》,他的微博中的很多理念对程序员来说很有价值,下面就是他的部分语录。

  • 做架构时,为了避免混淆,我尽量让技术名词有固定的定义。系统、子系统、对象三者是包含的关系。系统暴露接口(Port),子系统暴露服务,对象暴露方法(ie. PASIO 的「操作」)。而接口、服务、方法又有各自的定义,他们主要的差异是在「影响范围」和「业务完整性」上。

  • 想让系统灵活有弹性,可以通过配置(configuration)。简单的配置,只能设置一些常量,无法改变流程。这种情况下,如果想改变流程,就必须通过插件(前提是系统有事先设计插件的接入方式)…. 我不倾向于用插件,我倾向于用更弹性的配置来改变流程,这时候通常需要配合定义一个 DSL 。

  • 我写代码时把对象内分成五部分:PASIO。Properties 用来读写对象内的数据;Attributes 是对象内所封装的数据;States 用来查询对象(和参数)是否符合状态;Instructions 是基本的方法;Operations 是有复杂逻辑的方法。我还限定 PASIO 五者间的调用关系,我发现这么做有助于把对象配置化。

  • Attribute 是真正保存数据的地方,最好是通过 Property 进行访问。对于 Object Model 来说,Attribute 就是 Field;对于 Relational Model 来说,Attribute 就是 Column;对于 Associative Model 来说,Attribute 就是 Association。

  • 设计 DSL 的时候,我先想像自己是使用此领域语言的专家,且没有计算机编程能力,然后设想一个场景,再用最方便的方式描写此场景。描写方式尽量描述目的(What),而非描述做法(How)。几个场景的描述之后,就可以归纳出一个简单有弹性的 DSL。

  • 软件开发的难度在于「如何将需求化为设计」,而不是「如何将设计化为代码」。能将设计化为代码的只能称为码农,能将需求化为设计的才是软件工程师。而架构师,更是要把非功能需求(例如高并发、高可用、可维护性、扩展性)的设计考虑进来。

  • 那些担心技术做不久的人,他们的担心是有道理的,因为他们的技术确实做不久;而那些不担心技术做不久的人,他们不担心也是有道理的,因为他们的技术确实可以做很久 … 一个人的心态决定了他的职业生涯。

好用到逆天的 Android Studio

在好多做 Android 的朋友的怂恿下,终于开始尝试使用 Android Stuio,这一用就放不下了,不仅颜值高(Darcula简直大爱,我是黑色控),而且好用到逆天,听说 RubyMine 和 Android Stuio 同样基于 IntelliJ IDEA,以后开发 Rails 的 IDE也要切换到 RubyMine 了,话不多说,下面是我喜欢的小技巧。

Help

每次打开 Android Studio 都会弹出 Tip of the Day,都是一些好用的技巧,每日必读,强裂推荐!还有Help菜单下的Default Keymap Reference,会在线显示所有快捷键,下载后存到我的 Kindle Voyage 里,时不时看下:)。

做过开发的都知道版本控制,其中 Diff 功能可以查看两次提交的差异,不过 Android Studio 不使用版本控制也实现了这个功能:右键调出正在编辑的文件,弹出上下文菜单,选择 Local History –> Show History.

Local History

Live Templates

Lambda转换

快速查看定义

⌘-Y查看 class

好用易忘的快捷键

Optimize imports 清除无效包引用 ⌃⌥O

折叠(所有)代码块

⌘-⌘—

展开(所有)代码块

⌘=⌘+

显示被调用的地方

⌥F7 Find Usages

弹出调用层级

⌃⌥h

包裹代码块

⌘⌥T Surround With

Switcher

⌃⇥ Switcher 一旦它被打开,只要你按住ctrl键,你可以使用数字或字母快捷键快速导航。

我最爱的特性

Find Action

类似Alfred,在输入框里输入要关键字,回车后即执行菜单功能

Recent Files

⌘E

Recent Edited Files

⌘⇧E

智能选区

⌥up ⌥down

Jump to Last Window

  1. 返回编辑器
  2. ⇧⎋ 返回编辑器并关闭当前窗口
  3. F12 再次打开刚刚关闭的窗口(我修改成 ⌃F12

Select In…

Go by Reference Actions

Surround With..

⌘⌥t

Unwrap/Remove

highlight

合适的Git Commit怎么写

人们总是关心为什么做多过于做了什么,这种观念同样出现在写Git Commit的信息上。下面是我总结出来的写提交信息的最佳实践(听烂了都)。

黄金环路

我最近重看了一次最喜欢的TED演讲,由Simon Sinek主讲,主题是《如何鼓舞士气》。

他解释了一个非常简单的观念:人们关心领导者为什么做,而不是做什么。这大概也适用于Git的提交信息。

The Golden Circle

Simon画了上面这张图(要比我画的好的多),解释道:伟大的公司有明确的愿景并且真的在实现它。回到我们的代码,每段代码都有自己的意图,我们要做的就是如何实现它。

为什么这么做?

你可能觉得奇怪,这有什么大不了的?是不是扯远了。如果你曾经在一个团队工作过,或者经历过一个长期的项目,在查阅之前的代码时,经常会搞不懂为什么那么写。于是你使用git blame一路查找到提交信息,让人焦虑的是,最终你只会发现:这段代码和提交信息不是你写的。其实你是想知道当初为什么这么写?

更进一步,我见过的绝大多数提交信息都有这个问题,也包括我在内,不久前,还在为我的提交信息洋洋自得,因为每次写之前,我都会仔细考虑,怎么让它尽可能的简单明了。

惊喜时刻

一天,我正和Craig Savolainen(导师&我认识的最佳开发者)结队编程,我写了一段自以为很像样的提交信息,而他说这没用并解释道:

Anyone can see WHAT you did just by looking at the code. But the code can never tell someone WHY you did it.
代码可以告诉别人你做了什么,但不能告诉别人你为什么这么做。

对啊!我立即使用新的风格来写提交信息,不过这需要一些时间来适应,因为提交前,我总是在想这段代码的目的什么,为什么要这么做,这的确很费脑力。好消息是我适应了,已经变得流畅自然了,我发现了一个模式,就是使用“should”。

示例

下面是一个实例。我用”做什么风格“写了提交信息:

Card view controller added.

如果我用“为什么做”风格重写呢?

User should be able to see the card before editing it.
在编辑前,用户应该先查看。

看到了吗?重写后的版本很容易看出这次提交原因以及一些上下文信息,这对以后维护这些代码的人是很有价值的。当他有问题的时候,就会找到我。同时这对我也有利,当重新阅读之前写的代码时,能帮助我想起提交时的场景!

结论

写提交信息的时候,用“为什么做”替代“做什么”对自己和团队都是有益的。在通往软件工匠的道路上,小的进步汇集起来都将是一大步。如果你认同或者有更好的想法,请与我们联系。

Happy exploring!

旁注

很明显,这里虽然以Git为例子,但同样适用于其它版本控制系统。我原本想使用“code commit”,但是经过大家的反馈,我决定用“Git commit”,看起来更明了些。

译自 Writing Great Git Commit Messages

兵器谱之Keyboard Maestro

Keyboard Maestro是一款类似Alfred的App,但一点也不逊色后者,它对Automator进行了封装,方便用户的使用。

Firefox快捷设置

使用浏览器时,我个人非常不习惯网页的白色背景,感觉非常刺眼,于是手动调成灰色,但个别网页不能正常显示,这时只能手动来切换,不管是鼠标点击还是键盘操作都非常烦锁。

Duang!有请Keyboard Maestro(简写成KM)出场,先在KM的编辑器里为我使用的Firefox添加一个Group并命名,在Avaliable in the following applications下拉菜单中选中Firefox,这样之后的操作只会在Firefox中生效,减少全局冲突;接着在中间的Macros栏中点击加号按钮添加一个Macro,取名为Backgroud Color White group

KM有许多触发动作的机制,这里我设置一个快捷键⌃⌘I,当切换到Firefox中并按下这个快捷捷,就会触发一系列自定义的action。点击New Action,弹出选择列表,作为一个键盘党,按照Firefox修改背景颜色的键盘顺序,添加Interface Control中的Type a keystroke就可以了。问题来了,当我进入常规选项卡里进行设置后,再触发前面设置的快捷键,预设的一系列键盘操作就会在常规里面跳转。