Android 生命周期架构组件与 RxJava 完美协作

本文主要讲述了 Android 生命周期架构组件如何与 RxJava 协作,从此过上了幸福生活的故事。

涉及的内容有:

  • Android 生命周期架构组件
  • LiveData 有哪些惊艳特性
  • 如何使用 RxJava 中的 Observable 代替 LiveData,同时拥有它的那些惊艳特性
  • Android 架构组件中的 ViewModel
  • 如何使用 RxJava 优雅地实现 ViewModel

2017-10 月更新:更新到 Support Library 26.1+,建议在 PC 下阅读

Handling Lifecycles

Android 的生命周期自上古时代以来就是个噩梦般的存在,很多难以察觉,莫名其妙的 BUG 就与之相关。处理不好,很容易导致内存泄漏和应用崩溃。譬如在 Activity 状态保存(Activity 的 onSaveInstanceState 被框架调用)后执行 Fragment 事务,将会导致崩溃。

谷歌官方推出的生命周期 架构组件,似乎终于能让我们睡个好觉了。 现在 Support Library 26.1+ 已经集成了这个生命周期组件。

buildscript {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}
dependencies {
    //  using Support Library 26.1+
    compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:support-v4:26.1.0'
    compile 'com.android.support:design:26.1.0'
}

android.arch.lifecycle 开发包提供的类和接口,能让我们构建生命周期感知(lifecycle-aware)组件 —— 这些组件可以基于 Activity 或 Fragment 的当前生命周期自动调整它们的行为。

从图中我们可以看到 LifecycleOwner 是 Lifecycle 的持有者,通常是一个 Activity 或 Fragment。想要获取 Lifecycle 只能通过 LifecycleOwner 的 getLifecycle 方法。Lifecycle 是可观察的,它可以持有多个 LifecycleObserver。

Lifecycle

可以看到,Lifecycle 是整个宇宙的核心。

Lifecycle 内部维护了两个枚举,一叫 Event,另一个叫 State。

Event 是对 Android 组件(Activity 或 Fragment)生命周期函数的映射,当 Android Framework 回调生命周期函数时,Lifecycle 会检查当前事件和上一个事件是否一致,如果不一致,就根据当前事件计算出当前状态,并通知它的观察者(LifecycleObserver)生命状态已经发生变化。观察者收到通知后,通过 Lifecycle 提供的 getCurrentSate 方法来获取刚刚 Lifecycle 计算出来的当前状态,观察者根据这个状态来决定要不要执行某些操作。

LifecycleOwner

LifecycleOwner 是个接口,只有一个 getLifecycle() 方法。

它从个体类(例如,Activity 和 Fragment)中抽象出生命周期所有权。这样,我们编写的生命周期感知组件,就可以在任何遵循了 LifecycleOwner 协议的类中使用,而不需要管它是 Activity 还是 Fragment。

版本 26.1+ 支持包中的 AppCompatActivity 或 Fragment 已经直接或间接地实现了 LifecycleOwner。当然,你也可以编写自己的 LifecycleOwner。

LifecycleObserver

那些可以使用 Lifecycle 的类被称为生命周期感知组件。谷歌提倡那些需要和 Android 生命周期协作的类库提供生命周期感知组件,这样客户端代码就可以很容易集成这些类库,而不需要客户端手动管理类库中和 Android 生命周期相关的代码。

实现 LifecycleObserver 的类便是生命周期感知组件。实现起来也并不复杂。

public class MyObserver implements LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void onResume() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void onPause() {
    }
}
owner.getLifecycle().addObserver(new MyObserver());

打个注解就可以监听对应的生命周期事件了,别忘了调用 owner.getLifecycle().addObserver() 把观察者注册到 Lifecycle.

LiveData

终于,我们惊艳的主角登场了。

LiveData 是一个 observable 数据的持有类,和普通的 observable 不同,LiveData 是生命周期感知的,意味着它代表其它应用组件譬如 Activity、Fragment、Service 的生命周期。这确保了 LiveData 只会更新处于活跃状态的组件。

LiveData 通过内部类的形式实现了 LifecycleObserver,它整个工作流程大概是这样的:

  1. 将实现了 LifecycleObserver 的内部类注册到 owner 的 Lifecycle。
  2. LifecycleObserver 监听了 Lifecycle 所有的生命周期事件
  3. 当有生命周期事件发生时,检查 Lifecycle 的状态是否至少是 STARTED 来判断 lifecycle 是否处于活跃状态
  4. 当维护的值被改变时,如果 Lifecycle 处于活跃状态,通知观察者(实现了 android.arch.lifecycle.Observer 接口的对象),否则什么也不做
  5. 当 Lifecycle 从非活跃状态恢复到活跃状态时,检查维护的值是否在非活跃期间有更新过,如果有,通知观察者
  6. 当 Lifecycle 处于完结状态 DESTROYED 时,从 Lifecycle 中移除 LifecycleObserver。

很显然,LiveData 是响应式的,令人惊艳的是,它有效解决了后台回调和 Android 生命周期的上古谜题。

LiveData 使用起来是这样的:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // do async operation to fetch users
    }
}

现在,Activity 可以访问这个列表如下:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

它还提供了 Transformations 来帮助我们转换 LiveData 所持有的数据。

Transformations.map()

LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});

Transformations.switchMap()

private LiveData<User> getUser(String id) {
  ...;
}

LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );

熟悉 RxJava 的小伙伴,是不是觉得少了些什么?

在处理数据和业务上,LiveData 远远没有 RxJava 强大,然而 LiveData 在 Android 生命周期的表现上也确实令人惊艳。有没有什么办法将两者合一?

谷歌为我们提供了 LiveDataReactiveStreams 来在 LiveData 和 RxJava 之间切换。不过没什么用。

Live

有没有办法能让 RxJava 像 LiveData 那样感知 Android 生命周期,拥有 LiveData 那些优点?

LiveData 以内部类的方式实现了 LifecycleObserver, RxJava 通过 ObservableTransformer 的方式实现 LifecycleObserver。

这正是 Live 的由来。至此,令人惊艳的 LiveData 完成了它的使命。

Live 使用起来是这样的

protected void onCreate(Bundle savedInstanceState) {
    mObservable
            .compose(Live.bindLifecycle(this))
            .subscribe();
}

没有回调,只有优雅的流。

熟悉 RxLifecycle 的小伙伴会发现,Live 和 RxLifecycle 不仅使用方式一致,而且功能似乎也一样。

不,它们有各自的使用场景。

Live 像 LiveData 那样,在页面不活跃时不会投递数据,当页面重新活跃,补投最新的数据,如果有的话。 Live 只会在 Lifecycle 发出 DESTROYED 事件时终结整个流。 总之 Live 拥有像 LiveData 那样的生命周期感知。

RxLifecycle 无论何时,只要流还没有终结,都会投递数据。RxLifecycle 可以指定在特定的生命周期事件发生时终结流,例如在 onStop 时。

我们来看看 Live 自带的 demo。

我们会以 startActivityForResult 的方式开启 SecondActivity

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivityForResult(intent, 100);
            }
        });

SecondActivity 上有个计时器,当按 Home 键退到后台时,日志停止打印,当重新回到前台时,日志又重新打印。 而流从来没有结束过。像 LiveData 那样惊艳。

Observable.interval(1, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .compose(Live.<Long>bindLifecycle(this))
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(@NonNull Long aLong) throws Exception {
                        Log.w(TAG, String.valueOf(aLong));
                        textView.setText(String.valueOf(aLong));
                    }
                });

按下 SecondActivity 上的返回按钮时,调用 setResult(RESULT_OK) ,返回 MainActivity。MainActivity 在 onActivityResult 中模拟了数据更新,这个更新要求 MainActivity 显示一个 Fragment。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    subject.compose(Live.<String>bindLifecycle(this))
            .subscribe(new Consumer<String>() {
                @Override
                public void accept(@NonNull String s) throws Exception {
                    showFragment();
                }
            });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 100 && resultCode == Activity.RESULT_OK) {
        subject.onNext("show fragment");
    }
}

在以往,如果处理不好,这个操作会导致应用崩溃。

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState 

但是现在,噩梦已经过去。 Live 让生活更美好。

ViewModel

故事大概讲了一半。让我们继续。

看到 ViewModel 这个词,就会让人想起 MVVM,ViewModel 正是 MVVM 架构里面的一个重要角色。

MVVM 按职责把类分为三种角色,分别是 View,ViewModel,Model。ViewModel 正好处于中间,是连接 View 和 Model 的桥梁。

  • View 只做两件事情,一件是根据 ViewModel 中存储的状态渲染界面,另外一件是将用户操作转发给 ViewModel。用一个公式来表述是这样的:view = render(state) + handle(event)。
  • ViewModel 也只做两件事。一方面提供 observable properties 给 View 观察,一方面提供 functions 给 View 调用,通常会导致 observable properties 的改变,以及带来一些额外状态。observable properties 是指 LiveData、Observable(RxJava) 这种可观察属性。View 正是订阅了这些属性,实现模型驱动视图。functions 是指可以用来响应用户操作的方法或其它对象,ViewModel 不会也不应该自己处理业务,它通过 functions 把业务逻辑的处理委派给 Model。用一个公式来表述是这样的:viewmodel = observable properties + functions。
  • Model 是整个应用的核心。它包含数据以及业务逻辑,网络访问、数据持久化都是它的职责。用一个公式来表述是这样的:model = data + state + business logic。

在 Android 中,View 包含 Activity 和 Fragment,由于 Activity 和 Fragment 可以销毁后重建,因此要求 ViewModel 在这个过程中能够存活下来,并绑定到新的 Activity 或 Fragment。架构组件提供了 ViewModel 来帮我们实现这点。

小伙伴们请注意区分,本文中的 ViewModel 可能是指 MVVM 中的 VM,也可能是指一个叫 ViewModel 的 Java 类。

当 Activity 或 Fragment 真正 finish 的时候,框架会调用 ViewModel 中的 onCleared 方法,我们需要在这个方法里面清除不再使用的资源。

官方实现让 ViewModel 在配置变化,譬如屏幕旋转,能够存活下来的方法,和我们前面说的 Lifecycle 无关,而是一种上古魔法。这种魔法自 Fragment 诞生以来就出现了,它的名字叫做 Headless Fragments

RxCommand

我们先前提及 ViewModel 提供 functions 给 View 调用,通常会导致 observable properties 的改变,以及带来一些额外状态。譬如,我们的 function 需要拉取一个用户列表,除了可能会导致表示用户列表的属性发生变化,在这个过程当中,还会出现一些额外状态,比如表示拉取是否正在执行的 executing,表示拉取可能发生异常的 errors,表示拉取动作是否可以执行的 enabled。

如果这些额外状态也需要 ViewModel 去维护,那么 ViewModel 就无法专注于自身的核心业务了。

RxCommand 一方面封装了这些 function,另一方管理了这些额外状态。

使用 RxCommand 之前的代码可能是这样的:

public class MyViewModel extends ViewModel {
    private final UserRepository userRepository;

    // observable properties
    private final Observable<List<User>> users;

    private final Observable<Boolean> executing;
    private final Observable<Boolean> enabled;
    private final Observable<Throwable> errors;

    public MyViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
        users = PublishSubject.create();
        executing = PublishSubject.create();
        enabled = PublishSubject.create();
        errors = PublishSubject.create();
    }

    // function    
    public void loadUsers() {
        enabled.onNext(false);
        executing.onNext(true);

        userRepository
            .getUsers()
            .doOnNext(usersList -> users.onNext(userList))
            .doOnError(throwable -> errors.onNext(throwable))
            .doFinally(() -> {
                executing.onNext(false);
                enabled.onNext(true);
            })
            .subscribe();                   
    }
}

一大堆模版代码。如果 ViewModel 还负责加载其它东西,那场景不敢想象。

使用 RxCommand 的代码看起来是这样的:

public class MyViewModel extends ViewModel {

    // function
    public final RxCommand<List<User>> usersCommand;

    public MyViewModel(final UserRepository userRepository) {

        usersCommand = RxCommand.create(o -> {
                return userRepository.getUsers();
            });
    }
}

ViewModel 中的代码很清爽。在 Activity 中是这么用的:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);

        model.usersCommand
                .switchToLatest()
                .observeOn(AndroidSchedulers.mainThread())
                .compose(Live.bindLifecycle(this))
                .subscribe(users -> {
                    // update UI
                });

        model.usersCommand
                .executing()
                .compose(Live.bindLifecycle(this))
                .subscribe(executing -> {
                    // show or hide loading
                })

        model.usersCommand
                .errors()
                .compose(Live.bindLifecycle(this))
                .subcribe(throwable -> {
                    // show error message
                });
    }
}

如果 ViewModel 中有多个 command 的话,可以通过 RxJava 的 merge 操作符统一处理 executing 和 error。

在代码中,我们看到了 Live,它和 RxCommand 真是绝配。其实本文讲的就是 Live(Android 生命周期架构组件)和 RxCommand (RxJava) 协作,过上幸福生活的故事。

让我们来看下 RxCommand 附带的 demo。

很普遍但不简单的需求。这里面有两个输入框,以及两个按钮。当手机号码输入合法时,获取验证码的按钮才会被点亮。一旦获取验证码的操作正在执行以及执行成功后开始倒数,获取验证码按钮一直处于不可点击状态,当获取验证码失败或者倒数结束后,获取验证码按钮才恢复可点状态。只有手机号码和验证码输入都合法时,登录按钮才会被点亮。

尤其是,当屏幕旋转时,所有的状态和操作都保留着,当 Activiy 重新创建时,绑定到新的 Activity。而所有这些,都是 LiveRxCommand 以及 ViewModel 共同协作的结果。

如果你想去看 源码,推荐使用 Octotree 这个 Chrome 插件,可以直接在浏览器上看。如果你喜欢,clone 下来改改跑跑也是不错的选择。

我们来分析下源码吧。

ViewModel 被设计用来存储和管理 UI 相关的数据,在我们的 LoginViewModel 中,phoneNumbercaptcha 被用来保存用户输入的手机号码以及验证码。

public class LoginViewModel extends ViewModel{
    public final Variable<CharSequence> phoneNumber;
    public final Variable<CharSequence> captcha;

    public LoginViewModel() {
        phoneNumber = new Variable<>("");
        captcha = new Variable<>("");
    }
}
public class LoginActivity extends AppCompatActivity implements LifecycleRegistryOwner{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        RxTextView
                .textChanges(phoneNumberEditText)
                .compose(Live.bindLifecycle(this))
                .subscribe(viewModel.phoneNumber::setValue);

       RxTextView
                .textChanges(captchaEditText)
                .compose(Live.bindLifecycle(this))
                .subscribe(viewModel.captcha::setValue);
    }
}

这样,我们就接收了用户输入。

RxTextView 是 JakeWharton 开源的 RxBinding 中的组件。用于将 Android UI 控件带入流的世界。

Variable 是对 BehaviorSubject 的封装。

我们是用纯 java 的方式实现数据绑定,而不是使用 data-binding 这样的库。

当点击按钮时,会触发 ViewModel 中对应的 function,我们分别为获取验证码按钮和登录按钮定义了相应的 command。 我们来看看登录按钮相关的 command 在 ViewModel 中是如何定义的:

public class LoginViewModel extends ViewModel{
    private RxCommand<Boolean> _loginCommand;

    // 用于检测手机号码或验证码是否合法
    private Observable<Boolean> _captchaValid;
    private Observable<Boolean> _phoneNumberValid;

    public LoginViewModel() {
         // 这里只是简单判断用户输入的长度
        _captchaValid = captcha.asObservable().map(s -> s.toString().trim().length() == 6);
        _phoneNumberValid = phoneNumber.asObservable().map(s -> s.toString().trim().length() == 11);
    }

    public RxCommand<Boolean> loginCommand() {
        if (_loginCommand == null) {

            // 因为登录按钮需要手机号码和验证码都合法时才能点亮,因此把它们合成一个流
            Observable<Boolean> loginInputValid = Observable.combineLatest(
                    _captchaValid,
                    _phoneNumberValid,
                    (captchaValid, phoneValid) -> captchaValid && phoneValid);

            // 第一个参数是一个发射 boolean 的流,
            // 当发射的值为 true 时,command 处于可执行状态,反之则不然,
            // 这个参数可以为 null,表示 command 是否可以执行不受外界影响。
            // 第二个参数是个 lambda,它接受一个参数 o,这个参数是 command 被执行时传入。
            // lambda 返回一个 Observable,通常直接返回业务层调用的结果
            _loginCommand = RxCommand.create(loginInputValid, o -> {
                String phone = this.phoneNumber.value().toString();
                String captcha = this.captcha.value().toString();
                return login(phone, captcha);
            });
        }
        return _loginCommand;
    }

    private Observable<Boolean> login(String phoneNumber, String code) {
          // ...
    }
}

我们来看看在 Activity 中如何使用:

RxCommandBinder
        .bind(loginButton, viewModel.loginCommand(), Live.bindLifecycle(this));

这样就把登录按钮和 loginCommand 绑定了。当用户手机号码和验证码都合法时,登录按钮才处于可点击状态,当登录任务开始执行时,按钮将处于不可点击状态,直到登录任务结束,成功或失败,按钮才可再次处于可点击状态,如果此时输入还合法的话。

当处于登录任务执行期间,想要显示个 loading ,怎么办呢?在 Activity 中:

viewModel.loginCommand()
        .executing()
        .compose(Live.bindLifecycle(this))
        .subscribe(executing -> {
            if (executing) {
                // show loading UI
            } else {
                // hide loading UI
            }
        });

就算屏幕旋转期间,仍然能正确展示。

让我们来看看获取验证码相关的 command,这里面的逻辑比较复杂。这里用到了一个隐藏的 command,它是用来负责倒计时的。

在 ViewModel 中

public RxCommand<String> captchaCommand() {
    if (_captchaCommand == null) {
         // 只有手机号码输入合法以及不在倒数时,获取验证码按钮才可点击
        Observable<Boolean> enabled = Observable.combineLatest(
                _phoneNumberValid,
                countdownCommand().executing(),
                (valid, executing) -> valid && !executing);

        _captchaCommand = RxCommand.create(enabled, o -> {
            String phone = _phoneNumber.blockingFirst().toString();

            // 调用获取验证码的业务逻辑
            Observable fetchCode =  fetchCaptcha(phone);

            // 获取验证码成功后,应该开始倒数,
            // 这里,我们手动执行 countdown command
            Observable countdown =  Observable.defer(() -> 
                        countdownCommand()
                            .execute(null)    // 手动执行 command
                            .ignoreElements() // 忽略返回值
                            .toObservable()
                    );
             // 利用 concat 操作符把获取验证码和倒计时的流串起来   
             // 获取验证码成功后,会开始倒计时,如果失败,则不会
            return Observable.concat(fetchCode, countdown);
        });
    }
    return _captchaCommand;
}

private Observable<String> fetchCaptcha(String phoneNumber) {
    return Observable.timer(2, TimeUnit.SECONDS)
            .map(i -> "your captcha is 123456.");
}

public RxCommand<String> countdownCommand() {
    if (_countdownCommand == null) {
    // 利用 RxJava 实现一个倒计时
    _countdownCommand = RxCommand.create(o -> Observable
            .interval(1, TimeUnit.SECONDS)
            .take(20) // from 0 to 19
            .map(aLong -> "fetch " + (19 - aLong) + "'"));
    }
    return _countdownCommand;
}

在 Activity 中

// 绑定获取验证码按钮和 captchaCommand
RxCommandBinder
        .bind(captchaButton, viewModel.captchaCommand(), Live.bindLifecycle(this));

// 如果获取成功了,就通知用户                
viewModel.captchaCommand()
        .switchToLatest()
        .observeOn(AndroidSchedulers.mainThread())
        .compose(Live.bindLifecycle(this))
        .subscribe(result -> Toast.makeText(LoginActivity.this, result, Toast.LENGTH_LONG).show()); 

 // 倒计时开始时,改变获取验证码按钮上的文字       
 viewModel.countdownCommand()
        .switchToLatest()
        .observeOn(AndroidSchedulers.mainThread())
        .compose(Live.bindLifecycle(this))
        .subscribe(s -> captchaButton.setText(s));

// 获取验证码成功、失败或倒计时结束,都会重置获取验证码按钮的文字
viewModel.captchaCommand()
        .executing()
        .compose(Live.bindLifecycle(this))
        .subscribe(executing -> {
            if (executing) {
                captchaButton.setText("Fetch...");
            } else {
                captchaButton.setText("Fetch Captcha");
            }
        });  

Activity 和 ViewModel 中的代码加起来 250 行左右,就能实现你在视频中看到的效果。

RxCommand 不一定需要通过 RxCommandBinder 把它和某个 View(按钮)绑定起来,也可以手动执行,比如在下拉刷新控件的回调中直接调用 command.execute(null)

MVVM 与 展示层

MVVM 架构放在一个更大的架构范畴中,只是一个展示层的架构,只是解决了 View 和 ViewModel 之间的关系。然而,在一个应用中,Model 才是核心。标准的三层架构和整洁架构,都将焦点放在 Model 身上。只懂 MVVM 是处理不好的业务的。这意味着,我们还有很长的路要走,还有许多未知领域需要探索。

总结

本文中,我们介绍了 Android 生命周期架构组件LiveViewModelRxCommand

愿生活更美好!

Listen

一枚想做工程师的码农

深圳 Github
blog comments powered by Disqus