RxJava- RxAndroid在动态EditText上进行表单验证

时间:2016-09-04 09:26:49

标签: android validation rx-java rx-android

我的表单可以包含可变数量的EditText,需要在表单提交之前进行验证。我可以执行验证检查EditText是否固定在数字中,如下所示 -

Observable<CharSequence> emailObservable = RxTextView.textChanges(editEmail).skip(1);
Observable<CharSequence> passwordObservable = RxTextView.textChanges(editPassword).skip(1);

mFormValidationSubscription = Observable.combineLatest(emailObservable, passwordObservable,
                (newEmail, newPassword) -> {                   
                    boolean emailValid = !TextUtils.isEmpty(newEmail) && android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches();
                    if(!emailValid) {
                        emailInputLayout.setError(getString(R.string.error_invalid_email));
                        emailInputLayout.setErrorEnabled(true);
                    }else {
                        emailInputLayout.setError(null);
                        emailInputLayout.setErrorEnabled(false);
                    }

                    boolean passValid = !TextUtils.isEmpty(newPassword) && newPassword.length() > 4;
                    if (!passValid) {
                        passwordInputLayout.setError(getString(R.string.error_invalid_password));
                        passwordInputLayout.setErrorEnabled(true);
                    } else {
                        passwordInputLayout.setError(null);
                        passwordInputLayout.setErrorEnabled(true);
                    }

                    return emailValid && passValid;
                }).subscribe(isValid ->{
                    mSubmitButton.setEnabled(isValid);
                });

但是现在由于输入数量可变,我尝试创建Observable<CharSequence>Observable.combineLatest()的列表,但我仍然坚持继续这样做。

List<Observable<CharSequence>> observableList = new ArrayList<>();

        for(InputRule inputRule : mMaterial.getRules()) {
            View vInputRow = inflater.inflate(R.layout.item_material_input_row, null, false);

            StyledEditText styledEditText = ((StyledEditText)vInputRow.findViewById(R.id.edit_input));
            styledEditText.setHint(inputRule.getName());

            Observable<CharSequence> observable = RxTextView.textChanges(styledEditText).skip(1);
            observableList.add(observable);

            linearLayout.addView(vInputRow);
        }

        Observable.combineLatest(observableList,......); // What should go in place of these "......" 

如何为每个输入字段执行有效的charsequence检查。我查看了flatMap()map()filter()方法,但我不知道如何使用它们。

3 个答案:

答案 0 :(得分:6)

我有一个解决方案,不使用lambda表达式(因为我无法使用lambdas编译它)。

根据需要使用相同的运算符:

public static <T, R> Observable<R> combineLatest(List<? extends Observable<? extends T>> sources, FuncN<? extends R> combineFunction)

Observable.combineLatest(observableList, new FuncN<Boolean>() {
    @Override
    public Boolean call(Object... objects) {
        boolean isValid = true;
        CharSequence input;
        for (int i = 0; i < objects.length; i++) {
            input = (CharSequence) objects[i];
            switch (i) {
                case 1:
                    //First text field value
                    break;
                case 2:
                    //Second text field value
                    break;
                default:
                    isValid = false;
            }
        }
        return isValid;
    }
})

lambda表达式不起作用的原因可能是函数second parametercombineLatest(...)

public interface FuncN<R> extends Function {
    R call(Object... args);
}

根据this post实施Arbitrary Number of Arguments很难做到,需要创建变通办法。 RxJava v2与Java 8兼容,并且具有combineLatest

的不同实现

答案 1 :(得分:6)

是的,您在.combineLatest()中处理了Observable的abitrary数量,但仍有解决方法。我对这个问题感兴趣并提出了以下解决方案 - 我们可以存储有关某些数据源的信息 - 最后一个值和源ID(字符串和资源ID)以及将所有数据隧道传输到一些公共管道中。为此,我们可以使用PublishSubject。我们还需要跟踪连接状态,因为我们应该在订阅时将Subscription保存到每个源,并在我们取消订阅该源时将其切断。 我们存储来自每个源的最后数据,因此我们可以告诉用户刚刚发出新值的源,回调将只包含源ID。用户可以按源ID获取任何源的最后一个值。 我想出了以下代码:

import android.util.Log;
import android.widget.EditText;

import com.jakewharton.rxbinding.widget.RxTextView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import rx.Observable;
import rx.Subscription;
import rx.functions.Action1;
import rx.subjects.PublishSubject;

public class MultiSourceCombinator {
    String LOG_TAG = MultiSourceCombinator.class.getSimpleName();
    /**
     * We can't handle arbitrary number of sources by CombineLatest, but we can pass data along
     * with information about source (sourceId)
     */
    private static class SourceData{
        String data = "";
        Integer sourceId = 0;
    }

    /**
     * Keep id of source, subscription to that source and last value emitted
     * by source. This value is passed when source is attached
     */
    private class SourceInfo{
        Subscription sourceTracking;
        Integer sourceId;
        SourceData lastData;

        SourceInfo(int sourceId, String data){
            this.sourceId = sourceId;
            // initialize last data with empty value
            SourceData d = new SourceData();
            d.data = data;
            d.sourceId = sourceId;
            this.lastData = d;
        }
    }

    /**
     * We can tunnel data from all sources into single pipe. Subscriber can treat it as
     * Observable<SourceData>
     */
    private PublishSubject<SourceData> dataDrain;

    /**
     * Stores all sources by their ids.
     */
    Map<Integer, SourceInfo> sources;

    /**
     * Callback, notified whenever source emit new data. it receives source id.
     * When notification is received by client, it can get value from source by using
     * getLastSourceValue(sourceId) method
     */
    Action1<Integer> sourceUpdateCallback;

    public MultiSourceCombinator(){
        dataDrain = PublishSubject.create();
        sources = new HashMap<>();
        sourceUpdateCallback = null;
        // We have to process data, ccoming from common pipe
        dataDrain.asObservable()
                .subscribe(newValue -> {
                    if (sourceUpdateCallback == null) {
                        Log.w(LOG_TAG, "Source " + newValue.sourceId + "emitted new value, " +
                                "but used did't set callback ");
                    } else {
                        sourceUpdateCallback.call(newValue.sourceId);
                    }
                });
    }

    /**
     * Disconnect from all sources (sever Connection (s))
     */
    public void stop(){
        Log.i(LOG_TAG, "Unsubscribing from all sources");
        // copy references to aboid ConcurrentModificatioinException
        ArrayList<SourceInfo> t = new ArrayList(sources.values());
        for (SourceInfo si : t){
            removeSource(si.sourceId);
        }
        // right now there must be no active sources
        if (!sources.isEmpty()){
            throw new RuntimeException("There must be no active sources");
        }
    }

    /**
     * Create new source from edit field, subscribe to this source and save subscription for
     * further tracking.
     * @param editText
     */
    public void addSource(EditText editText, int sourceId){
        if (sources.containsKey(sourceId)){
            Log.e(LOG_TAG, "Source with id " + sourceId + " already exist");
            return;
        }
        Observable<CharSequence> source = RxTextView.textChanges(editText).skip(1);
        String lastValue = editText.getText().toString();
        Log.i(LOG_TAG, "Source with id " + sourceId + " has data " + lastValue);
        // Redirect data coming from source to common pipe, to do that attach source id to
        // data string
        Subscription sourceSubscription = source.subscribe(text -> {
            String s = new String(text.toString());
            SourceData nextValue = new SourceData();
            nextValue.sourceId = sourceId;
            nextValue.data = s;
            Log.i(LOG_TAG, "Source " + sourceId + "emits new value: " + s);
            // save vlast value
            sources.get(sourceId).lastData.data = s;
            // pass new value down pipeline
            dataDrain.onNext(nextValue);
        });
        // create SourceInfo
        SourceInfo sourceInfo = new SourceInfo(sourceId, lastValue);
        sourceInfo.sourceTracking = sourceSubscription;
        sources.put(sourceId, sourceInfo);
    }

    /**
     * Unsubscribe source from common pipe and remove it from list of sources
     * @param sourceId
     * @throws IllegalArgumentException
     */
    public void removeSource(Integer sourceId) throws IllegalArgumentException {
        if (!sources.containsKey(sourceId)){
            throw new IllegalArgumentException("There is no source with id: " + sourceId);
        }
        SourceInfo si = sources.get(sourceId);
        Subscription s = si.sourceTracking;
        if (null != s && !s.isUnsubscribed()){
            Log.i(LOG_TAG, "source " + sourceId + " is active, unsubscribing from it");
            si.sourceTracking.unsubscribe();
            si.sourceTracking = null;
        }
        // source is disabled, remove it from list
        Log.i(LOG_TAG, "Source " + sourceId + " is disabled ");
        sources.remove(sourceId);
    }

    /**
     * User can get value from any source by using source ID.
     * @param sourceId
     * @return
     * @throws IllegalArgumentException
     */
    public String getLastSourceValue(Integer sourceId) throws IllegalArgumentException{
        if (!sources.containsKey(sourceId)){
            throw new IllegalArgumentException("There is no source with id: " + sourceId);
        }
        String lastValue = sources.get(sourceId).lastData.data;
        return lastValue;
    }

    public void setSourceUpdateCallback(Action1<Integer> sourceUpdateFeedback) {
        this.sourceUpdateCallback = sourceUpdateFeedback;
    }
}

我们可以在UI中使用它:

import android.app.Activity; 
import android.os.Bundle; 
import android.util.Log; 
import android.widget.EditText; 
import android.widget.Toast;

import butterknife.BindView; 
import butterknife.ButterKnife;

public class EdiTextTestActivity extends Activity {

    @BindView(R.id.aet_et1)
    public EditText et1;
    @BindView(R.id.aet_et2)
    public EditText et2;
    @BindView(R.id.aet_et3)
    public EditText et3;

    private MultiSourceCombinator multiSourceCombinator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit_text_test);
        ButterKnife.bind(this);

        multiSourceCombinator = new MultiSourceCombinator();
        multiSourceCombinator.setSourceUpdateCallback(id -> {
            Toast.makeText(EdiTextTestActivity.this, "New value from source: " + id  + " : " +
            multiSourceCombinator.getLastSourceValue(id), Toast.LENGTH_SHORT).show();
        });
    }

    @Override
    protected void onPause() {
        // stop tracking all fields
        multiSourceCombinator.stop();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        // Register fields
        multiSourceCombinator.addSource(et1, R.id.aet_et1);
        multiSourceCombinator.addSource(et2, R.id.aet_et2);
        multiSourceCombinator.addSource(et3, R.id.aet_et3);
    } 
}

答案 2 :(得分:1)

我使用R.Zagórski的答案作为如何使用Kotlin

的指导

这最终对我有用。

        val ob1 = RxTextView.textChanges(field1).skip(1)
        val ob2 = RxTextView.textChanges(field2).skip(1)
        val ob3 = RxTextView.textChanges(field3).skip(1)

        val observableList = arrayListOf<Observable<CharSequence>>()

        observableList.add(ob1)
        observableList.add(ob3)

        val formValidator = Observable.combineLatest(observableList, {
            var isValid = true
            it.forEach {
                val string = it.toString()
                if (string.isEmpty()) {
                    isValid = false
                }
            }
            return@combineLatest isValid
        })

        formValidator.subscribe { isValid ->
            if (isValid) {
                //do something
            } else {
                //do something
            }
        }