0

I'm trying to use RxJava to perform a validation on a EditText when I click a button but I'm having a hard time moving the validation to the ViewModel which would make testing much easier. I'm using RxBindings from Jake Wharton to get the UI input and RxJava2's Flowable.combineLatest with a PublishSubject to trigger the Flowable when I click a button on a AlertDialog. Here's what I got so far:

private Flowable<CharSequence> projectTitleObservable;
private final PublishSubject<CharSequence> createProjectClicked = PublishSubject.create();

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    final Context context = getActivity();

    View dialogView = LayoutInflater.from(context).inflate(R.layout.new_project, null);

    ButterKnife.bind(this, dialogView);

    projectTitleObservable = RxTextView.textChanges(projectNameEditText).toFlowable(BackpressureStrategy.LATEST);

    ConnectableFlowable <CharSequence> connectableFlowable = Flowable.combineLatest(createProjectClicked.toFlowable(BackpressureStrategy.LATEST), projectTitleObservable, (ignored, title) -> {
        boolean validTitle = !TextUtils.isEmpty(title);
        if (!validTitle) {
            projectNameEditText.setError("Project must have a name");
        }

        Timber.d("Hey, lambdas work! Look -> " + title);
        return title;
    })
            .publish();

    connectableFlowable.subscribe(test -> Timber.d(test.toString()));

    return new AlertDialog.Builder(context)
            .setTitle("Add new Project")
            .setView(dialogView)
            .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
                Timber.d("Ok clicked!");
                createProjectClicked.onNext("It works!");
            })
            .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> Timber.d("Cancel clicked!"))
            .create();
}

I'm only publishing and subscribing on the same place to make sure it actually works before moving the ConnectableFlowable to the ViewModel. The only log I got was the "Ok clicked!" which for me it means it was never subscribed. Any idea on why it's not subscribing?

davisjp
  • 730
  • 1
  • 11
  • 24
  • If you put a Log.d before return title does it get executed? – Leandro Borges Ferreira Oct 20 '17 at 19:51
  • I think you should call onNext inside your combineLastest closure – Leandro Borges Ferreira Oct 20 '17 at 19:51
  • There is a Timber.d (same as Log.d) before the return title and it never gets called. The idea was that I wanted to be able to get ConnectableFlowable executed when i call createProjectClicked.onNext, if the log is not being called then onNext won't be called inside the combineLatest either. – davisjp Oct 20 '17 at 20:03
  • I think the combined flowable won't publish an event unless both floawable publish an event. Did both flowables emitted itens? – Leandro Borges Ferreira Oct 20 '17 at 20:45
  • Humm... I ran another test with 2 EditTexts instead of a PublishSubject and I got the Flowables to emit error msgs only after interacting with both. I guess I will have to find a better way to block the Ok button with the PublishSubject. Do you know of a better way of validating the fields on the ViewModel? Thanks anyway. – davisjp Oct 20 '17 at 21:36
  • Well, you could create a validator that emits false if the text is not OK and true if everything is fine. You can use it with combineLastest just like you are doing and combine with your button. But remove the toFlowable methods, this is not a situation where you need backpressure... no one types this fast. – Leandro Borges Ferreira Oct 20 '17 at 21:44
  • you may want to use RxBindings from Jake Wharton, it will make things easier .. – Mohamed Ibrahim Oct 21 '17 at 14:20
  • @LeandroBorgesFerreira if you could put that in code as an answer I'll accept it. – davisjp Oct 21 '17 at 19:09
  • @MohamedIbrahim I'm already using it, if you know of a better way to use it for this purpose, please let me know. – davisjp Oct 21 '17 at 19:11
  • @DavisJP check my answer, I think it solves your problem in a clean way – Leandro Borges Ferreira Oct 24 '17 at 16:17

2 Answers2

0

I can achieve this behavior using a flatMap. Here is a example of how to show a Toast if the text is valid:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val btnSubject = PublishSubject.create<Boolean>()
    val textObservable = RxTextView.textChangeEvents(editText)
            .filter { validateText(it.text().toString()) }

    btnSubject.flatMap { textObservable }
            .subscribe({
                Toast.makeText(this, "Text is correct", Toast.LENGTH_SHORT).show()
            })


    btn.setOnClickListener {
        btnSubject.onNext(true)
    }
}

private fun validateText(text: String) = text.length >= 5

If you need to show some feed back if the text is incorrect, you can change filter for a map and use it like this:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val btnSubject = PublishSubject.create<Boolean>()
    val textObservable = RxTextView.textChangeEvents(editText)
            .map { validateText(it.text().toString()) }

    btnSubject.flatMap { textObservable }
            .subscribe({ isTextCorrect ->
                if (isTextCorrect) {
                    Toast.makeText(this, "Text is correct", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this, "Text NOT is correct", Toast.LENGTH_SHORT).show()
                }
            })


    btn.setOnClickListener {
        btnSubject.onNext(true)
    }
}

private fun validateText(text: String) = text.length >= 5

I just tested this code, works just fine.

Happy code!

Leandro Borges Ferreira
  • 12,422
  • 10
  • 53
  • 73
  • This is very cool, not only solves the mess with the ConnectableFlowables but allows for calling methods outside the class. Thanks! – davisjp Oct 24 '17 at 21:24
  • I will add a java "translation" of the code above for people not familiar with Kotlin. – davisjp Oct 24 '17 at 21:25
0

Java version:

Show a Toast if the code is valid:

private Observable<TextViewTextChangeEvent> taskTitleObservable;
private final PublishSubject<Boolean> createTaskClicked = PublishSubject.create();

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ButterKnife.bind(this, view);

    taskTitleObservable = RxTextView.textChangeEvents(taskTitle)
    .filter(event -> validateText(event.text().toString()));

    createTaskClicked.flatMap(test -> taskTitleObservable).subscribe( aVoid -> {
        Toast.makeText(getActivity(), "Text is correct", Toast.LENGTH_SHORT).show();
    });

    RxView.clicks(fab).subscribe(aVoid -> {
        createTaskClicked.onNext(true);
    });
}

private Boolean validateText (String text) {
    return text.length() >= 5;
}

Feedback if the text is incorrect:

private Observable<Boolean> taskTitleObservable;
private final PublishSubject<Boolean> createTaskClicked = PublishSubject.create();

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ButterKnife.bind(this, view);

    taskTitleObservable = RxTextView.textChangeEvents(taskTitle)
            .map(event -> validateText(event.text().toString()));

    createTaskClicked.flatMap(test -> taskTitleObservable).subscribe( isTextCorrect -> {
        if (isTextCorrect) {
            Toast.makeText(getActivity(), "Text is correct", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(getActivity(), "Text is incorrect", Toast.LENGTH_SHORT).show();
        }
    });

    RxView.clicks(fab).subscribe(aVoid -> {
        createTaskClicked.onNext(true);
    });
}

private Boolean validateText (String text) {
    return text.length() >= 5;
}
davisjp
  • 730
  • 1
  • 11
  • 24