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 的当前生命周期自动调整它们的行为。
从图中我们可以看到 LifecycleOwner 是 Lifecycle 的持有者,通常是一个 Activity 或 Fragment。想要获取 Lifecycle 只能通过 LifecycleOwner 的 getLifecycle 方法。Lifecycle 是可观察的,它可以持有多个 LifecycleObserver。
Lifecycle
可以看到,Lifecycleopen in new window 是整个宇宙的核心。
Lifecycle 内部维护了两个枚举,一叫 Event,另一个叫 State。
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,它整个工作流程大概是这样的:
- 将实现了 LifecycleObserver 的内部类注册到 owner 的 Lifecycle。
- LifecycleObserver 监听了 Lifecycle 所有的生命周期事件
- 当有生命周期事件发生时,检查 Lifecycle 的状态是否至少是
STARTED
来判断 lifecycle 是否处于活跃状态 - 当维护的值被改变时,如果 Lifecycle 处于活跃状态,通知观察者(实现了
android.arch.lifecycle.Observer
接口的对象),否则什么也不做 - 当 Lifecycle 从非活跃状态恢复到活跃状态时,检查维护的值是否在非活跃期间有更新过,如果有,通知观察者
- 当 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。
我们会以 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
但是现在,噩梦已经过去。 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
方法,我们需要在这个方法里面清除不再使用的资源。
官方实现让 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 window 和 RxCommandopen in new window 以及 ViewModelopen in new window 共同协作的结果。
如果你想去看 源码open in new window,推荐使用 Octotree 这个 Chrome 插件,可以直接在浏览器上看。如果你喜欢,clone 下来改改跑跑也是不错的选择。
我们来分析下源码吧。
ViewModel 被设计用来存储和管理 UI 相关的数据,在我们的 LoginViewModel 中,phoneNumber
和 captcha
被用来保存用户输入的手机号码以及验证码。
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 window 、Liveopen in new window 、 ViewModelopen in new window 、RxCommandopen in new window。
愿生活更美好!