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 事务,将会导致崩溃。

谷歌官方推出的生命周期 架构组件open in new window,似乎终于能让我们睡个好觉了。 现在 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.lifecycleopen in new window 开发包提供的类和接口,能让我们构建生命周期感知(lifecycle-aware)组件 —— 这些组件可以基于 Activity 或 Fragment 的当前生命周期自动调整它们的行为。

android-lifecycle-rxjava-2021-10-15-01-25-38

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

Lifecycle

可以看到,Lifecycleopen in new window 是整个宇宙的核心。

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

android-lifecycle-rxjava-2021-10-15-01-26-18

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

LifecycleOwner

LifecycleOwneropen in new window 是个接口,只有一个 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

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

LiveDataopen in new window 是一个 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 生命周期的表现上也确实令人惊艳。有没有什么办法将两者合一?

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

Live

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

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

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

Live 使用起来是这样的

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

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

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

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

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

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

我们来看看 Live 自带的 demo。

android-lifecycle-rxjava-2021-10-15-01-26-53

我们会以 startActivityForResult 的方式开启 SecondActivity

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

android-lifecycle-rxjava-2021-10-15-01-27-13

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

android-lifecycle-rxjava-2021-10-15-01-27-34

按下 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");
    }
}

android-lifecycle-rxjava-2021-10-15-01-27-54

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

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

但是现在,噩梦已经过去。 Liveopen in new window 让生活更美好。

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。架构组件提供了 ViewModelopen in new window 来帮我们实现这点。

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

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

android-lifecycle-rxjava-2021-10-15-01-28-21

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

RxCommand

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

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

RxCommandopen in new window 一方面封装了这些 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 还负责加载其它东西,那场景不敢想象。

使用 RxCommandopen in new window 的代码看起来是这样的:

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。

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

让我们来看下 RxCommandopen in new window 附带的 demo。

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

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

如果你想去看 源码open in new window,推荐使用 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 开源的 RxBindingopen in new window 中的组件。用于将 Android UI 控件带入流的世界。

Variable 是对 BehaviorSubject 的封装。

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

当点击按钮时,会触发 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 生命周期架构组件open in new windowLiveopen in new windowViewModelopen in new windowRxCommandopen in new window

愿生活更美好!

android-lifecycle-rxjava-2021-10-15-01-30-33

上次更新: 6/30/2022, 6:52:25 AM