背景/Background
前阵子不久,Jake Wharton 在Devoxx 的演讲: The State of Managing State with RxJava 中提出了一个类似于Redux的 Full Rx 的App 结构。 如下图:
整个结构全部由RxJava控制 state。 和传统MVX结构类似,也是大致分UI层(View),中间层(Presenter/ViewModel/Controller或者我更喜欢叫Translator)和数据层(Model)。大致流程如下:
- Ui层(View)层将用户输入数据打包成UiEvent传递给中间层
- 中间层(Translator)将Event处理成对应的Action交给数据处理层。
- 处理结果打包成对应的Result交还给Translator
- Translator将数据结果打包成对应的UiModel交换给Ui做对应的Ui显示。
实现/Demo
我们先一步一步写个Demo看下这个结构的优缺点吧!
为了方便,我直接使用Android Studio提供的LoginActivity模板。
我们的目的是要做一个注册界面,为了简化只有用户名,密码。首先我们来定义Event:
public class AuthEvent {
public final static class SignUpEvent extends AuthEvent {
private final String username;
private final String password;
public SignUpEvent(String username, String password) {
this.username = username;
this.password = password;
}
//... getters
}
}
这里SignUpEvent继承自AuthEvent是为了统一逻辑。这样我们可以在一整条stream里实现我们所有的逻辑。
我们在Ui层将这个Event打包(这里我使用RxBinding):
Observable click = RxView.clicks(mEmailSignInButton)
.map(ignore -> new SignUpEvent(mEmailView.getText().toString(),
mPasswordView.getText().toString()));
这样我们每次点击按钮就会发射一个SignUpEvent出来。
再来我们定义我们的UiModel,我们首先要想好,我们的Ui到底有几种状态,我们将各种状态提前定义。我大致觉得我们需要四种状态:
- idle 初始状态,就是用户第一次进入的状态
- inProcess 状态,也就Ui界面等待注册是否成功的状态
- success 状态,注册成功 进行下一步操作
- fail 状态,注册失败,返回失败信息。
根据这四种状态,我们来定义UiModel:
public class AuthUiModel {
private final boolean inProcess;
private final boolean usrValidate;
private final boolean pwdValidate;
private final boolean success;
private final String errorMessage;
private AuthUiModel(boolean inProcess, boolean usrValidate, boolean pwdValidate, boolean success, String errorMessage) {
this.inProcess = inProcess;
this.usrValidate = usrValidate;
this.pwdValidate = pwdValidate;
this.success = success;
this.errorMessage = errorMessage;
}
public static AuthUiModel idle() {
return new AuthUiModel(false, true, true, false, "");
}
public static AuthUiModel inProcess() {
return new AuthUiModel(true, true, true, false, "");
}
public static AuthUiModel success() {
return new AuthUiModel(false, true, true, true, "");
}
public static AuthUiModel fail(boolean username, boolean password, String msg) {
return new AuthUiModel(false, username, password, false, msg);
}
//... getters
}
再来是Model层,我们这里用一个简单的AuthManager来管理,解耦出来后这里可以替换成任意你喜欢的注册方式:
public class AuthManager {
private SignUpResult result;
private Observable observable = Observable.fromCallable(() -> result)
//延迟2s发送结果,模拟网络请求延迟
.delay(2000, TimeUnit.MILLISECONDS);
public Observable signUp(SignUpAction action) {
//检查用户名是否合法
if (TextUtils.isEmpty(action.getUsername()) || !action.getUsername().contains("@")) {
result = SignUpResult.FAIL_USERNAME;
}
//检查密码合法
else if (TextUtils.isEmpty(action.getPassword()) || action.getPassword().length() < 9) {
result = SignUpResult.FAIL_PASSWORD;
} else {
//检查结束,返回注册成功的信息
// TODO: createUser
result = SignUpResult.SUCCESS;
}
return observable;
}
}
这里SignUpAction里定义了我们注册所有需要的信息,代码和SignUpEvent几乎雷同。但是分离的好处是可以对数据进行在处理或者合并打包等等。
Ui和Model都准备好了,我们开始我们的Translator部分。 Translator部分主要又ObservableTransformer组成。 将各个部件组装,具体如下:
public final ObservableTransformer signUp
//上游是UiEvent,封装成对应的Action
= observable -> observable.map(event -> new SignUpAction(event.getUsername(), event.getPassword()))
//使用FlatMap转向,进行注册
.flatMap(action -> authManager.signUp(action)
//扫描结果
.map(signUpResult -> {
if (signUpResult == SignUpResult.FAIL_USERNAME) {
return AuthUiModel.fail(false, true, "Username error");
}
if (signUpResult == SignUpResult.FAIL_PASSWORD) {
return AuthUiModel.fail(true, false, "Password error");
}
if (signUpResult == SignUpResult.SUCCESS) {
return AuthUiModel.success();
}
//TODO Handle error
throw new IllegalArgumentException("Unknown Result");
})
//设置初始状态为loading。
.startWith(AuthUiModel.inProcess())
//设置错误状态为error,防止触发onError() 造成断流
.onErrorReturn(error -> AuthUiModel.fail(true, true, error.getMessage())));
这样我们在Activity里 将各个部分通过Translator组装:
disposables.add(click.compose(translator.signUp)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(authUiModel -> {
//载入进度条
mProgressView.setVisibility(authUiModel.isInProcess() ? View.VISIBLE : View.GONE);
//判断用户名/密码是否合法
if (!authUiModel.isPwdValidate()) {
mPasswordView.setError(authUiModel.getErrorMessage());
} else {
mPasswordView.setError(null);
}
if (!authUiModel.isUsrValidate()) {
mEmailView.setError(authUiModel.getErrorMessage());
} else {
mEmailView.setError(null);
}
//是否成功
if (authUiModel.isSuccess()) {
Toast.makeText(this, "CreateUser SuccessFull", Toast.LENGTH_SHORT)
.show();
}
}));
很明显的看到,在Activity中 只有Ui相关的处理,而中间的逻辑通过translator解耦出来,对Activity不可见。
问题/Issues
但是,问题来了。这里些许Bug.由于我们使用Transformer. 每次转屏的时候会通过RxView来生成新的Observable.这样我们的translator并没有复用,还是绑定在了生命周期上。那么如何解决?
我们设想一下,如果中间的Translator可以随时接受下游的订阅而且无论下游是否有订阅,他都可以一直运行,这样不就在下游彻底解耦了吗?这种特性的Observable我在上一篇文章中说到是ConnectableObservable。这里我们使用Replay(1)。这样我们就每次重新订阅,也会获得最近的一次UiModel,再也不用担心转屏/内存重启。
下游解决了,那上游呢?如果上游每次调用这个Transformer,每次还是一个新的Observable啊。理想的情况应该是我们有一个中间人,他不断接受Ui层传过来的UiEvent然后交给我们Transformer, 这样我们就能一直复用我们的Transformer。也就是他既作为一个Observer订阅上游UiEvent又作为一个Observable,给下游传递数据。那么答案呼之欲出,我们需要一个Subject作为中间人。
改善后的Translator代码如下:
public class AuthTranslator {
private AuthManager authManager;
private Subject middle = PublishSubject.create();
private Observable authUiModelObservable
= middle.map(event -> new SignUpAction(event.getUsername(), event.getPassword()))
//使用FlatMap转向,进行注册
.flatMap(action -> authManager.signUp(action)
//扫描结果
.map(signUpResult -> {
if (signUpResult == SignUpResult.FAIL_USERNAME) {
return AuthUiModel.fail(false, true, "Username error");
}
if (signUpResult == SignUpResult.FAIL_PASSWORD) {
return AuthUiModel.fail(true, false, "Password error");
}
if (signUpResult == SignUpResult.SUCCESS) {
return AuthUiModel.success();
}
//TODO Handle error
throw new IllegalArgumentException("Unknown Result");
})
//设置初始状态为loading。
.startWith(AuthUiModel.inProcess())
//设置错误状态为error,防止触发onError() 造成断流
.onErrorReturn(error -> AuthUiModel.fail(true, true, error.getMessage())))
.replay(1)
.autoConnect();
public final ObservableTransformer signUp
//上游是UiEvent,封装成对应的Action
= observable -> {
//中间人切换监听
observable.subscribe(middle);
return authUiModelObservable;
};
public AuthTranslator(AuthManager authManager) {
this.authManager = authManager;
}
}
这样我们刚才说的两个Bug就解决了。而且即使我们在请求中转屏,也毫无问题。
总结
实践一下这个结构确实有很多优点。
- 将一整条state stream解耦分成几块,但又保持了一整条的结构。
- 相比传统MVX模式,多次控制翻转(Ioc),解耦更彻底
- 由于RxJava强大的操作符群。可以实现很多意想不到的功能
缺点也蛮明显:
- 我个人对这个架构理解也不是特别深入,中间的middle部分虽然用Subject 但是确实有其不稳定性,比如onError/onComplete会停止这个Subject造成断流
- 由于解耦彻底,造成需要很多辅助类,茫茫多的boilerplate。 不过这个在kotlin上有很好的发挥,sealed class,when 等语法几乎是为其量身定做。
- 难,真的难。比传统MVP,甚至MVVM�需要更清晰,更合理的设计。不提前想好use case就开始写几乎是不可能的。而且RxJava如果不熟悉,调试起来确实很难。经常不能定位到代码。最好做单元测试各个模块。
最后附上这个Demo 的GitHub Repo: RxAuthDemo