原文链接: https://www.ykode.com/2015/02/20/android-frp-rxjava-retrolambda.html
在你的Android应用中,每一个UI控件都在不停的产生事件。而你所写的代码也正是用来处理这些事件的,例如用户点击按钮或者一个从后端返回的一个REST响应。通常情况下,我们会通过对应的事件Listener来捕获并处理这些事件,例如OnClickListener
。这个模式并没有问题,但是,当你尝试跟踪并处理多种状态的时候,就会显得不那么优雅了。注册页面以及表单中多个字段的校验就是一个很常见的场景。为了解决这个问题,Netflix开发了一个小巧的library——RxJava,而它对应在android的library叫作RxAndroid。这个library提供了函数式响应式编程的扩展,可以用来在android应用中处理复杂的异步事件处理。
函数式响应编程(Functional Reactive Programming)
函数式响应编程是由两种编程模式组合而成。简而言之,它们是:
- 函数式编程:一种通过map,filter和reduce等操作来转换或者组合一系列的事件流。而事件本身是不可变的。
- 响应式编程:这种编程模式主要用来简化数据流动和变换传播。
通俗的来说,函数式响应编程就是电子表格(Excel)应用。你可以设计一系列公式,当任意一个时刻,某一个单元格的数据改变的时候,结果也会相应的发生改变。公式本身并不发生改变,但是某一单元格发生的变动会立即传播到结果的单元格上去。
RxJava
在Java环境(包括Android)中,RxJava提供了函数式响应编程的框架。RxJava是基于微软的.NET的响应式框架Rx,并将其移植到了JVM中。
Observable(观察事件)和Observer(观察者)
RxJava使用了观察者模式。RxJava中,最基本的模块就是Observer。一个Observable会产生数据,而Subscriber(订阅者)会消耗这些数据。RxJava和通常的观察者模式不同的是:一个观察事件在被订阅之前,是不会开始任何工作的。
下面是RxJava中一个最基本的“Hello World”代码:
package com.ykode.belajar;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action1;
public class Hello {
public static void main(String [] args) {
Observable<String> helloObservable = Observable.create( // .... [1]
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> sub) {
sub.onNext("Hello, World!");
sub.onCompleted();
};
});
helloObservable.subscribe( new Action1<String>() { // .... [2]
@Override
public void call(String s) {
System.out.println("Subscriber : " + s);
}
});
}
}
WTF! 一个hello world程序居然需要26行代码!但是,这段代码是使用Java7的风格写的,因此会很冗长。暂且先不考虑其冗余性。那么这段代码到底给做了什么?
helloObservable
会产生”Hello World”数据。helloObservable
被订阅了。这个Subscriber实现了Action1
。它会接受一个String参数并打印出来。
我们可以添加一个Subscriber,也同样打印接受到的信息。
helloObservable.subscribe( new Action1<String> {
@Override
public void call(String s) {
System.out.println("Sub1 : " + s);
}
});
helloObservable.subscriber( new Action1<String> {
@Override
public void call(String s) {
System.out.println("Sub2 : " + s);
}
});
这段代码会添加两个Subscriber并且都打印”Hello World!”。
Sub1 : Hello, World!
Sub2 : Hello, World!
如果把”Hello World!”改成”Bonjour Monde!”。那么所有的Subscriber都会打印修改后的信息。这就是构成响应式编程的一个特性:变换传播(change propagation)。
转换(Transformation)
函数响应式编程提供了转换Observable的功能,例如map和filter。为了更清晰的说明这个功能,让我们修改Observabled部分,让它从Iterable中读取数据。
package com.ykode.belajar;
import rx.Observable;
import rx.Subscriber;
import rx.schedulers.Schedulers;
import rx.functions.Action1;
import java.util.Arrays;
import java.util.List;
public class Hello {
// Getting current thread name
public static String currentThreadName() {
return Thread.currentThread().getName();
}
public static void main(String [] args) {
List<String> names = Arrays.asList("Didiet", "Doni", "Asep", "Reza",
"Sari", "Rendi", "Akbar");
Observable<String> helloObservable = Observable.from(names); // ... [1]
helloObservable.subscribe( new Action1<String>() {
@Override
public void call( String s ) {
String greet = "Sub 1 on " + currentThreadName() +
": Hello " + s + "!";
System.out.println(greet); // ... [2]
}
});
}
这个代码会把names
中的每一条数据都打印一遍。
- 这个Observable是通过from方法来构造的。Subscriber会一条接一条的获取到Utterable中的数据。
- Subscriber会把这些数据打印出来。
结果如下:
Sub 1 on main: Hello Didiet!
Sub 1 on main: Hello Doni!
Sub 1 on main: Hello Asep!
Sub 1 on main: Hello Reza!
Sub 1 on main: Hello Sari!
Sub 1 on main: Hello Rendi!
Sub 1 on main: Hello Akbar!
如果想把字符串都变成大写的,那么使用Observable的map方法。
import rx.functions.Func1; // ..... [1]
// ... code redacted ...
helloObservable.map( new Func1<String, String>() {
@Override
public String call( String s ) {
return s.toUpperCase();
}
}).subscribe( new Action1<String>() {
@Override
public void call( String s ) {
String greet = "Sub 1 on " + currentThreadName() +
": Hello " + s + "!";
System.out.println(greet);
}
});
Subscriber会打印大写的字符串:
Sub 1 on main: Hello DIDIET!
Sub 1 on main: Hello DONI!
Sub 1 on main: Hello ASEP!
Sub 1 on main: Hello REZA!
Sub 1 on main: Hello SARI!
Sub 1 on main: Hello RENDI!
Sub 1 on main: Hello AKBAR!
Lambda表达式
从上面的代码可以看到,转换和订阅的代码都十分冗余。你必须实现Action类(Action1
,Action2
等),如果你想返回一个值,就必须实现Func类(Func1
,Func2
等)。这些类都是范型,你必须制定参数和返回类型来构建它们。它们也都只提供一个方法。这种冗余的代码恶心的不少人,包括我在内。不过在Java 8中,我们可以使用lambda表达式。通过lambda表达式,我们的代码会精简很多:
helloObservable.map( s -> s.toUpperCase() )
.subscribe( s -> {
String greet = "Sub 1 on " + currentThreadName() +
": Hello" + s + "!";
System.out.println(greet);
});
为了使代码可读性更强,我们可以通过map来构造字符串。
helloObservable.map( s -> s.toUpperCase() )
.map( s -> "Sub 1 on " + currentThreadName() + ": Hello " + s + "!")
.subscribe( s -> System.out.println(s));
我们把greet
变量缩短成了s
。新的代码可读性甚至更强。
Android
重要更新:这篇文章中的代码已经过时了,而且可能产生内存泄露。我已经新的博客中修改了泄露问题并且更新了编译脚本和依赖。
接下来用实际场景来介绍响应式编程在Android中的使用。假设有一个注册页面,包含两个注册信息。一个是邮箱,一个是用户名。这个注册页面包含如下行为:
- 两个注册信息的部分初始为空
- 如果注册信息符合标准格式,那么文本颜色为黑色
- 用户名必须包含4个以上的字符
- 邮箱必须满足规范的正则表达式
- 如果注册信息不符合标准格式,那么文本颜色为红色
- 只有当两个注册信息都合法的时候,注册按钮才会生效
如果你开发过Android程序,那么你可以想象到会需要实现很多匿名内部类来追踪文本的状态,和设置注册按钮的行为。
准备
使用Android Studio创建一个简单的程序。
接着使用模版,新建一个Activity和一个Fragment,最低版本为Jelly Bean(API 16)。然后使用LinearLayout添加两个Edittext和一个注册Button。最终效果如下图所示。对应控件的id用红色文字标出。
添加RxAndroid和Retrolambda
想要在Android上使用RxJava,可以直接使用原生的RxJava。但是,RxAndroid提供了一个更加良好的封装。修改build.gradle
并添加如下几个依赖:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'io.reactivex:rxandroid:0.24.0'
}
这样一来,你就添加了RxAndroid的依赖病可以开始使用响应式编程的特性了。
那么lambda表达式呢?不幸的是,Android的SDK目前还不支持lambda表达式,因为它只兼容到Java 6,所以在编译成dex的时候会产生错误。如果想要保持兼容性的话,那么就必须使用传统的冗余方式来实现各种接口了。
RetroLambda可以用来将Java 8的代码转化为Java 6的代码,从而可以使用lambda表达式。需要提醒的是,想要更简洁的代码,就需要一定的风险。RetroLambda的实现方式比较投机,而且也无法保证在今后的Android SDK中可以继续适用。但是目前为止,直到SDK 5.0,它都没有出现问题。
在这里,让我们使用RetroLambda。只需要在build.gradle
添加对应的依赖就可以了,不过这次是要在根目录的build.gradle
添加,而不是模块的。
dependencies {
classpath 'com.android.tools.build:gradle:1.0.1'
classpath 'me.tatarka:gradle-retrolambda:2.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
接下来,我们在模块中使用对应的插件:
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
如果你使用了Android Studio,那么它默认使用Java 7来编译,无法支持lambda表达式。为了避免这一点,在build.gradle
的android
部分添加相关的设置
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
这会强制IDE使用Java 8去做语法高亮和解析。最后,我们需要在proguard-rules.pro
中,添加相应防止混淆的规则。
-dontwarn java.lang.invoke.*
捕获事件
在之前的例子中,我们的观察事件是一个单一的值和一组存放在iterable中的值。在Android中,观察事件是一组响应事件。我们从edtUserName
开始事件。我们希望捕获用户输入的字符并在ADB中打印出来。Android观察者的设置应该在onStart中完成。
@Override
protected void onStart() {
super.onStart();
Observable<OnTextChangeEvent> userNameText =
WidgetObservable.text((EditText) findViewById(R.id.edtUserName));
userNameText.subscribe( e -> Log.d("[Rx]", e.text().toString()));
}
当我们启动app的时候,会得到如下输出:
过滤事件
来做一点点修改。只有在输入的字符大于4的时候,我们才打印信息。这也是用户名合法的基本条件。
userNameText.filter( e -> e.text().length() > 4)
.subscribe( e -> Log.d("[Rx]", e.text().toString()));
再次启动app就可以看到不同之处了:
到这里,我们就已经建立了一个非常简单的数据流。这也是响应式编程的一种独特的展现方式。为了让它更好理解,可以用下面这个图来表示:
映射(Mapping)/转换(Transforming)事件
现在,我们来为每一个信息(邮箱和用户名)添加校验逻辑。和上面的逻辑类似,构造一个数据流来实现。在这里,使用map来实现代码。
为了实现这个功能,我们为每一个EditText定义一个Observables。校验规则为:
- 用户名必须包含4个以上的字符
- 邮箱必须满足规范的正则表达式
@Override
protected void onStart() {
super.onStart();
final Pattern emailPattern = Pattern.compile(
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"); // ..... [1]
EditText unameEdit = (EditText) findViewById(R.id.edtUserName);
EditText emailEdit = (EditText) findViewById(R.id.edtEmail);
Observable<Boolean> userNameValid = WidgetObservable.text(unameEdit) // [2]
.map(e -> e.text())
.map(t -> t.length() > 4);
Observable<Boolean> emailValid = WidgetObservable.text(emailEdit)
.map(e -> e.text())
.map(t -> emailPattern.matcher(t).matches());
emailValid.map(b -> b ? Color.BLACK : Color.RED)
.subscribe( color -> emailEdit.setTextColor(color)); // ... [3]
userNameValid.map( b -> b ? Color.BLACK : Color.RED)
.subscribe( color -> userNameEdit.setTextColor(color));
}
代码的主要逻辑如下:
- 使用
java.util.regex
包来对邮箱作正则判断。 - 给用户名和邮箱添加Observable。
- 我们将校验的Observable映射到颜色的Observable,然后订阅它们来改变颜色。
结果如下:
分裂和合并事件
最后,我们希望实现,当两个输入信息都合法的时候,注册按钮能够自动开启。为了实现这个效果,我们需要把两个Observable合并成一个。在这里,我们使用combineLatest方法。在OnStart中添加代码。
Button registerButton = (Button) findViewById(R.id.buttonRegister);
Observable<Boolean> registerEnabled =
Observable.combineLatest(userNameValid, emailValid, (a,b) -> a && b);
registerEnabled.subscribe( enabled -> registerButton.setEnabled(enabled));
combineLatest的参数是多个Observable以及一个Func2实例(或者lambda表达式),这个Func2实例用来定义怎么合并两个Observable。合并之后,会产生一个新的Observable。
如效果所示,两个文本部分保持了原有的效果,并且它们的结果能够影响到注册按钮的开启或关闭。最终的流程图如下:
从流程图可以看出,我们把原来的数据源分开成了两个,并且把它们合并成了一个新的数据源。这个新的数据源可以控制注册按钮的状态。如果你使用RxAndroid和lambda表达式的话,所有的逻辑只用20行代码就可以完成了。
优化
我们的应用已经实现了所有的功能了。但是,仍然有一个细节的点可能会造成不必要的性能开销。已有的几个Subscirber(用户名校验,邮箱校验和注册按钮设置)都从两个EditText接受数据,这就没次用户输入都会需要处理订阅事件。我们可以稍微修改一下代码来改进这一点。
emailValid.doOnNext( b -> Log.d("[Rx]", "Email " + (b ? "Valid" : "Invalid")))
.map(b -> b ? Color.BLACK : Color.RED)
.subscribe(color -> emailEdit.setTextColor(color));
userNameValid.doOnNext( b -> Log.d("[Rx]", "Uname " + (b ? "Valid" : "Invalid")))
.map(b -> b ? Color.BLACK : Color.RED)
.subscribe(color -> userNameEdit.setTextColor(color));
// and the registerenabled
registerEnabled.doOnNext( b -> Log.d("[Rx]", "Button " + (b ? "Enabled" : "Disabled")))
.subscribe( enabled -> registerButton.setEnabled(enabled));
当我们运行app的时候,可以发现,每次用户输入了信息,都会打印出对应的日志。即使在信息的合法性并没有发生改变的情况下,也同样会打印出来。
这不是理想的状态。正常情况下,只有当输入信息的合法性发生改变的时候,Subscriber才需要被调用。为了做到这一点,我们需要使用distinctUntilChanged方法。示例如下:
emailValid.distinctUntilChanged()
.doOnNext( b -> Log.d("[Rx]", "Email " + (b ? "Valid" : "Invalid")))
.map(b -> b ? Color.BLACK : Color.RED)
.subscribe(color -> emailEdit.setTextColor(color));
userNameValid.distinctUntilChanged()
.doOnNext( b -> Log.d("[Rx]", "Uname " + (b ? "Valid" : "Invalid")))
.map(b -> b ? Color.BLACK : Color.RED)
.subscribe(color -> userNameEdit.setTextColor(color));
// and registerEnabled
registerEnabled.distinctUntilChanged()
.doOnNext( b -> Log.d("[Rx]", "Button " + (b ? "Enabled" : "Disabled")))
.subscribe( enabled -> registerButton.setEnabled(enabled));
这种情况下,Subscriber只会在真正发生改变的情况下才会被调用。
结论
函数式响应编程是目前的热点,它在处理并发事件的时候有很大的优势。RxJava和RxAndroid更是一个高效的工具,这篇文章只是简单的介绍了一下这两个工具的使用方式。希望你能够通过这篇文章获得一定的了解,它们的思维方式可能需要一定的时间才能够理解。但是一旦理解了,就可以发现它其实很简单。Observable是一系列的事件,我们通过各种各样的操作(map, transform, combine等)来定义它们的最终行为。
Rx最主要的目标是使你的代码更加简洁,方便测试,且可读性更强。lambda表达式可以让代码可读性更强。通过连接Observer,我们可以规划出整体的流程。我认为这比定义handler以及内部匿名类要简洁很多。