Используя RxJava для проверки подлинности электронной почты, наблюдаемый дважды испускает

Я делаю простую регистрационную форму (адрес электронной почты и пароль), чтобы попытаться укрепить мой реактивный навык программирования. У меня возникли проблемы с проверкой правильности поля электронной почты, чтобы работать так, как я этого хочу.

Вот мой код:

final Observable<CharSequence> email = RxTextView.textChanges(emailView); Observable<Boolean> emailIsValid = email.map(new Func1<CharSequence, Boolean>() { @Override public Boolean call(CharSequence charSequence) { Log.d("asdf", "emailIsValid call: " + charSequence); return Pattern.matches(Patterns.EMAIL_ADDRESS.pattern(), charSequence); } }); RxView.focusChanges(emailView) .withLatestFrom(emailIsValid, new Func2<Boolean, Boolean, Boolean>() { @Override public Boolean call(Boolean hasFocus, Boolean emailIsValid) { return (!hasFocus && !emailIsValid); } }) .subscribe(new Action1<Boolean>() { @Override public void call(Boolean showError) { if (showError) { emailInputLayout.setError("Enter a valid email"); } else { emailInputLayout.setError(null); } } }); Observable<CharSequence> password = RxTextView.textChanges(passwordView); Observable.combineLatest(emailIsValid, password, new Func2<Boolean, CharSequence, Boolean>() { @Override public Boolean call(Boolean emailIsValid, CharSequence password) { Log.d("asdf", "valid: " + emailIsValid + ", password: " + password); return (emailIsValid && password.length() > 0); } }) .subscribe(RxView.enabled(loginButton)); 

И вот журнал:

emailIsValid call: emailIsValid call: valid: false, password: // I type 'j' emailIsValid call: j emailIsValid call: j valid: false, password: // I type 'a' emailIsValid call: ja emailIsValid call: ja valid: false, password:

Как вы можете видеть, emailIsValid вызывается дважды при каждом emailIsValid символа, а это значит, что он дважды выполняет регулярное выражение, что довольно расточительно.

Я посмотрел, как я могу сделать emailIsValid только один раз для каждого изменения, независимо от того, сколько подписчиков у него есть, и я нашел метод share() . Вот что происходит, когда я добавляю .share() в конец объявления emailIsValid :

emailIsValid call: // I type 'j' emailIsValid call: j valid: false, password: // I type 'a' emailIsValid call: ja valid: false, password:

Это решает проблему, но вызывает другую: в конце combineLatest функции combineLatest функции combineLatest , поэтому кнопка Login запускается, когда она должна быть отключена (выделена серым цветом).

Какой самый чистый способ решить эту проблему? Я думаю, что я хочу, чтобы он вел себя как BehaviorSubject , но я не уверен, что это лучший способ сделать это.

Вы можете использовать publish() и connect() .

 val email = RxTextView.textChanges(emailEditText) val emailIsValid = email.map { charSequence -> Log.d("asdf", "emailIsValid call: " + charSequence) Pattern.matches(Patterns.EMAIL_ADDRESS.pattern(), charSequence) }.publish() RxView.focusChanges(emailEditText) .withLatestFrom(emailIsValid) { hasFocus, emailIsValid -> (!hasFocus && !emailIsValid) } .subscribe { showError -> if (showError) { Log.d("asdf", "error") } } val password = RxTextView.textChanges(passwordEditText) Observable.combineLatest(emailIsValid, password) { emailIsValid, password -> Log.d("asdf", "valid: $emailIsValid, password: $password") (emailIsValid && password.length > 0) }.subscribe(RxView.enabled(button)) emailIsValid.connect() 

Или просто переключите порядок subscribe потому что это вызывает проблему.

 val email = RxTextView.textChanges(emailEditText) val emailIsValid = email.map { charSequence -> Log.d("asdf", "emailIsValid call: " + charSequence) Pattern.matches(Patterns.EMAIL_ADDRESS.pattern(), charSequence) }.share() val password = RxTextView.textChanges(passwordEditText) Observable.combineLatest(emailIsValid, password) { emailIsValid, password -> Log.d("asdf", "valid: $emailIsValid, password: $password") (emailIsValid && password.length > 0) }.subscribe(RxView.enabled(button)) RxView.focusChanges(emailEditText) .withLatestFrom(emailIsValid) { hasFocus, emailIsValid -> (!hasFocus && !emailIsValid) } .subscribe { showError -> if (showError) { Log.d("asdf", "error") } } 

Примечание: код находится в Kotlin, и более подробная информация о публикации / подключении находится по адресу http://www.introtorx.com/content/v1.0.10621.0/14_HotAndColdObservables.html#PublishAndConnect

Ваша проблема аналогична тому, что упоминается в разделе PublishAndConnect .:

Вторая подписка подписывается поздно и пропускает первую публикацию. Мы могли бы переместить вызов метода Connect () до тех пор, пока не будут сделаны все подписки. Таким образом, даже при вызове Thread.Sleep мы не будем подписываться на базовую информацию до тех пор, пока не будут сделаны обе подписки.

Я думаю, что здесь происходит следующее:

  • Первая subscribe – та, что находится в конце RxView.focusChange()... – вызывает подписку на emailIsValid (и, следовательно, также на email ).

  • email затем немедленно выдает текущее содержимое TextView качестве своего первого элемента, который, в свою очередь, проходит через emailIsValid и share и переходит к первому Subscriber (то есть к оператору withLatestFrom ).

  • Спустя некоторое время combineLatest вызывает другую подписку на emailIsValid . Поскольку emailIsValid – это share d, эта подписка не «проходит» по email и поэтому каждый элемент будет выходить только один раз.

  • Проблема заключается в том, что share ведет себя как PublishSubject : он просто PublishSubject любые будущие события всем подписчикам, но не воспроизводит ни одно из предыдущих.

В сумме это означает: когда приходит второй Subscriber ( combineLatest ), начальное значение уже прошло – оно было выпущено сразу после первой подписки. И следующее значение поступит только при изменении содержимого TextView .

Попробуйте replay(1).refCount() вместо share() в конце emailIsValid – это должно гарантировать, что каждый новый Абонент также получит последний предыдущий результат оценки, а также все будущие.

Я надеюсь, что это решает проблему и что мое объяснение имеет смысл.