12/08/2018, 16:27

Rx trong Kotlin (P3) - Login screen

Qua hai phần đầu tiên, bạn đã hiểu thế nào là Rx và cách áp dụng Rx trong Kotlin. Ở phần thứ 3 này, để hiểu hơn nó và có thể sử dụng với các thư viện binding có sẵn, chúng ta sẽ sử dụng RxKotlin trong một module cơ bản của app là module LogIn. Các yêu cầu của màn hình này là: - Độ dài bắt ...

Qua hai phần đầu tiên, bạn đã hiểu thế nào là Rx và cách áp dụng Rx trong Kotlin. Ở phần thứ 3 này, để hiểu hơn nó và có thể sử dụng với các thư viện binding có sẵn, chúng ta sẽ sử dụng RxKotlin trong một module cơ bản của app là module LogIn.

Các yêu cầu của màn hình này là:

- Độ dài bắt buộc là hơn 6 ký tự
- Check format của email nhập vào
- Thông báo lỗi sai cho user
- Verify cho mỗi ký tự nhập vào

Code của chúng ta sẽ được viết giống như thế này:

//username/email
emailObservable
-> lengthGreaterThanSix
-> verifyEmailPattern
-> retryIfSomethingFails
subscribe()
//password
passwordObservable
-> lengthGreaterThanSix
-> retryIfSomethingFails
subscribe()

Hàm validate độ dài lengthGreaterThanSix tạo bằng một ObservableTransformer với input và output. Trim chuỗi nhập vào, filter và check nếu độ dài ký tự > 6, nếu không đủ điều kiện sẽ throw exception. SingleOrError() chuyển Observable về một Single.

private val lengthGreaterThanSix = ObservableTransformer<String, String> { observable ->
        observable.map { it.trim() }
                    .filter { it.length > 6 }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Length should be greater than 6"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

Tương tự là validate email với hàm verifyEmailPattern.

private val verifyEmailPattern = ObservableTransformer<String, String> { observable ->
        observable.map { it.trim() }
                    .filter {
                        Patterns.EMAIL_ADDRESS.matcher(it).matches()
                    }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Email not valid"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

Ta sẽ thử test 2 hàm này xem sao nhé

Observable.just(temp: String)
            .compose(lengthGreaterThanSix)
            .compose(verifyEmailPattern)
            .subscribe({
                Timber.e("onNext: $it look good!")
            }, {
                Timber.e("onError: ${it.message}")
            }, {
                Timber.e("onComplete!")
            })

temp = abc, show ra message: onError: Length should be greater than 6 temp = 1234567, show message: onError: Email not valid temp = name@domain.com show message : onNext: name@domain.com look good! onComplete

Thế là ok rồi!

Tiếp đến chúng ta sẽ dựng layout cho màn hình login, giao diện đơn giản chỉ bao gồm 2 trường là email/password

Để dựng lên nó, phần layout ta làm như sau:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/linearLayout"
    android:layout_awidth="match_parent"
    android:layout_height="match_parent"
    android:background="#e3e3e3"
    android:orientation="vertical"
    tools:context=".LoginActivity">

    <TextView
        android:id="@+id/textView2"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:gravity="center"
        android:text="Welcome"
        android:textColor="#333333"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.design.widget.TextInputLayout
        android:id="@+id/emailWrapper"
        android:layout_awidth="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="12dp"
        app:layout_constraintBottom_toTopOf="@+id/passwordWrapper"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2"
        app:layout_constraintVertical_chainStyle="packed">

        <EditText
            android:id="@+id/editTextEmail"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:hint="Email"
            android:inputType="textEmailAddress" />

    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:id="@+id/passwordWrapper"
        android:layout_awidth="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toTopOf="@+id/buttonLogin"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/emailWrapper"
        app:passwordToggleEnabled="true">

        <EditText
            android:id="@+id/editTextPassword"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:hint="Password"
            android:inputType="textPassword" />

    </android.support.design.widget.TextInputLayout>

    <Button
        android:id="@+id/buttonLogin"
        android:layout_awidth="0dp"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="Login"
        android:textColor="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</android.support.constraint.ConstraintLayout>

Có nhiều thư viện để binding layout, ở đây ta dùng thư viện RxBinding rất dễ sử dụng

RxTextView.afterTextChangeEvents(editTextEmail)
            .skipInitialValue()
            .map {it.view().text.toString() }
            .compose(lengthGreaterThanSix)
            .compose(verifyEmailPattern)
            .subscribe({
                Timber.e("onNext: $it look good!")
            }, {
                Timber.e("onError: ${it.message}")
            }, {
                Timber.e("onComplete!")
            })

Bạn thấy nó có giống với cách chúng ta vừa test ở trên không? Thực ra có sự khác biệt nho nhỏ ở đây Với Observable.just("abc") stream sẽ thực hiện như sau

| — — — “abc” —→ (end)

Còn với RxTextView.afterTextChangeEvents(editTextEmail) :

| — — — “a” — “ab” — “abc” — →

So sánh 2 luồng, ta có thể thấy luồng đầu chỉ validate 1 item, còn luồng thứ 2 thì là 3, nó làm cho code bị fails. Cách giải quyết là ta cần sử dụng flatMap với những observables lồng nhau để xử lý riêng lẻ. code như sau:

private val lengthGreaterThanSix = ObservableTransformer<String, String> { observable ->
        observable.flatMap {
            Observable.just(it).map { it.trim() } // - abcdefg - |
                    .filter { it.length > 6 }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Length should be greater than 6"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

ta có thể thấy nó được hiểu như sau:

| —Obs(“a”)—Obs( “ab”) — Obs(“abc”)→

Làm thương tự với hàm verifyEmailPattern

private val verifyEmailPattern = ObservableTransformer<String, String> { observable ->
        observable.flatMap {
            Observable.just(it).map { it.trim() }
                    .filter {
                        Patterns.EMAIL_ADDRESS.matcher(it).matches()
                    }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Email not valid"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

Sau đây là full code của màn hình này:

private inline fun retryWhenError(crossinline onError: (ex: Throwable) -> Unit): ObservableTransformer<String, String> = ObservableTransformer { observable ->
        observable.retryWhen { errors ->
            errors.flatMap {
                onError(it)
                Observable.just("")
            }
        }
    }

private val lengthGreaterThanSix = ObservableTransformer<String, String> { observable ->
        observable.flatMap {
            Observable.just(it).map { it.trim() } // - abcdefg - |
                    .filter { it.length > 6 }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Length should be greater than 6"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

private val verifyEmailPattern = ObservableTransformer<String, String> { observable ->
        observable.flatMap {
            Observable.just(it).map { it.trim() }
                    .filter {
                        Patterns.EMAIL_ADDRESS.matcher(it).matches()
                    }
                    .singleOrError()
                    .onErrorResumeNext {
                        if (it is NoSuchElementException) {
                            Single.error(Exception("Email not valid"))
                        } else {
                            Single.error(it)
                        }
                    }
                    .toObservable()
        }
    }

RxTextView.afterTextChangeEvents(editTextEmail)
                .skipInitialValue()
                .map {
                    emailWrapper.error = null
                    it.view().text.toString()
                }
                .debounce(1, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread())
                .compose(lengthGreaterThanSix)
                .compose(verifyEmailPattern)
                .compose(retryWhenError {
                    emailWrapper.error = it.message
                })
                .subscribe()

RxTextView.afterTextChangeEvents(editTextPassword)
                .skipInitialValue()
                .map {
                    passwordWrapper.error = null
                    it.view().text.toString()
                }
                .debounce(1, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread())
                .compose(lengthGreaterThanSix)
                .compose(retryWhenError {
                    passwordWrapper.error = it.message
                })
                .subscribe()

Trên đây là module cơ bản khi bạn sử dụng RxKotlin, chúc các bạn code vui vẻ với bộ đôi Rx và Kotlin rất hay này. Thanks! Nguồn

0