Android中基于RxJava的响应式编程

原文链接: 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的风格写的,因此会很冗长。暂且先不考虑其冗余性。那么这段代码到底给做了什么?

  1. helloObservable会产生”Hello World”数据。
  2. 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中的每一条数据都打印一遍。

  1. 这个Observable是通过from方法来构造的。Subscriber会一条接一条的获取到Utterable中的数据。
  2. 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类(Action1Action2等),如果你想返回一个值,就必须实现Func类(Func1Func2等)。这些类都是范型,你必须制定参数和返回类型来构建它们。它们也都只提供一个方法。这种冗余的代码恶心的不少人,包括我在内。不过在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.gradleandroid部分添加相关的设置

  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));  


    }

代码的主要逻辑如下:

  1. 使用java.util.regex包来对邮箱作正则判断。
  2. 给用户名和邮箱添加Observable。
  3. 我们将校验的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以及内部匿名类要简洁很多。

猜你喜欢

转载自blog.csdn.net/hwz2311245/article/details/51276592