Rx trong Kotlin (P3) - Login screen
Bài đăng này đã không được cập nhật trong 3 năm
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_width="match_parent"
android:layout_height="match_parent"
android:background="#e3e3e3"
android:orientation="vertical"
tools:context=".LoginActivity">
<TextView
android:id="@+id/textView2"
android:layout_width="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_width="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_width="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_width="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_width="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_width="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
All rights reserved