背景/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:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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):
1 2 3
| Observable<SignUpEvent> click = RxView.clicks(mEmailSignInButton) .map(ignore -> new SignUpEvent(mEmailView.getText().toString(), mPasswordView.getText().toString()));
|
这样我们每次点击按钮就会发射一个SignUpEvent出来。
再来我们定义我们的UiModel,我们首先要想好,我们的Ui到底有几种状态,我们将各种状态提前定义。我大致觉得我们需要四种状态:
- idle 初始状态,就是用户第一次进入的状态
- inProcess 状态,也就Ui界面等待注册是否成功的状态
- success 状态,注册成功 进行下一步操作
- fail 状态,注册失败,返回失败信息。
根据这四种状态,我们来定义UiModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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来管理,解耦出来后这里可以替换成任意你喜欢的注册方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class AuthManager { private SignUpResult result; private Observable<SignUpResult> observable = Observable.fromCallable(() -> result) //延迟2s发送结果,模拟网络请求延迟 .delay(2000, TimeUnit.MILLISECONDS); public Observable<AuthResult.SignUpResult> 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组成。 将各个部件组装,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public final ObservableTransformer<SignUpEvent, AuthUiModel> 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组装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class AuthTranslator { private AuthManager authManager; private Subject<SignUpEvent> middle = PublishSubject.create(); private Observable<AuthUiModel> 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<SignUpEvent, AuthUiModel> 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