Android中基于RxJava的响应式编程
来源:互联网 发布:js 严格模式 eval 编辑:程序博客网 时间:2024/05/17 02:24
原文链接: 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中完成。
@Overrideprotected 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 registerenabledregisterEnabled.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 registerEnabledregisterEnabled.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以及内部匿名类要简洁很多。
- Android中基于RxJava的响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava(四)-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava(四:在Android中使用响应式编程)
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- 深入浅出RxJava四-在Android中使用响应式编程
- Java生成随机字符串(a-z,A-Z)
- android listview大量图片及时释放内存办法
- Linux上每个SCSI设备的最大LUN数目是多少(by quqi99)
- 4种存储结构
- https客户端证书.p12maven打包后tomcat启动不正确
- Android中基于RxJava的响应式编程
- 基于jsoup爬虫下载图库
- HttpClient与httpComponents、HttpPost、HttpGet
- iOS字典创建后,没有key。。。
- C++库文件解析(conio.h)
- Integer.valueOf(int i)与自动拆箱与装箱
- csv 读取 邮件附件读取
- Oracle单行函数
- Android获取随机三位数