拥抱RxJava(四):动手做一个Full Rx的 注册界面

96
W_BinaryTree
0.1 2017.04.25 17:30* 字数 1350

背景/Background

前阵子不久,Jake Wharton 在Devoxx 的演讲: The State of Managing State with RxJava 中提出了一个类似于Redux的 Full Rx 的App 结构。 如下图:

Redux

整个结构全部由RxJava控制 state。 和传统MVX结构类似,也是大致分UI层(View),中间层(Presenter/ViewModel/Controller或者我更喜欢叫Translator)和数据层(Model)。大致流程如下:

  1. Ui层(View)层将用户输入数据打包成UiEvent传递给中间层
  2. 中间层(Translator)将Event处理成对应的Action交给数据处理层。
  3. 处理结果打包成对应的Result交还给Translator
  4. 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<SignUpEvent> click = RxView.clicks(mEmailSignInButton)
        .map(ignore -> new SignUpEvent(mEmailView.getText().toString(),
                mPasswordView.getText().toString()));

这样我们每次点击按钮就会发射一个SignUpEvent出来。

再来我们定义我们的UiModel,我们首先要想好,我们的Ui到底有几种状态,我们将各种状态提前定义。我大致觉得我们需要四种状态:

  1. idle 初始状态,就是用户第一次进入的状态
  2. inProcess 状态,也就Ui界面等待注册是否成功的状态
  3. success 状态,注册成功 进行下一步操作
  4. 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<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组成。 将各个部件组装,具体如下:

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组装:

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<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就解决了。而且即使我们在请求中转屏,也毫无问题。

总结

实践一下这个结构确实有很多优点。

  1. 将一整条state stream解耦分成几块,但又保持了一整条的结构。
  2. 相比传统MVX模式,多次控制翻转(Ioc),解耦更彻底
  3. 由于RxJava强大的操作符群。可以实现很多意想不到的功能

缺点也蛮明显:

  1. 我个人对这个架构理解也不是特别深入,中间的middle部分虽然用Subject 但是确实有其不稳定性,比如onError/onComplete会停止这个Subject造成断流
  2. 由于解耦彻底,造成需要很多辅助类,茫茫多的boilerplate。 不过这个在kotlin上有很好的发挥,sealed class,when 等语法几乎是为其量身定做。
  3. 难,真的难。比传统MVP,甚至MVVM�需要更清晰,更合理的设计。不提前想好use case就开始写几乎是不可能的。而且RxJava如果不熟悉,调试起来确实很难。经常不能定位到代码。最好做单元测试各个模块。

最后附上这个Demo 的GitHub Repo: RxAuthDemo

关于Reactive Extensions
Web note ad 1