Rxjava 2.0 是一个流行的反应式编程库,它帮助无数的 android 开发人员使用更少的代码和更低的复杂性创建了高度响应的应用程序,尤其是在管理多个线程时。
如果您是众多转向 kotlin 的开发人员之一,那么这并不意味着您需要放弃 RxJava!
在本系列的第一部分中,我向您展示了如何从使用 Java 中的 RxJava 2.0 编程转变为使用Kotlin 中的 RxJava 编程。我们还研究了如何利用 RxKotlin 的扩展功能从您的项目中消除样板文件,以及避免许多开发人员在第一次使用带有 Kotlin 的 RxJava 2.0 时遇到的 SAM 转换问题的秘诀。
在第二部分中,我们将专注于 RxJava 如何通过使用 RxJava 2.0、RxAndroid 和 rxbinding 创建响应式 Android 应用程序来帮助解决您在实际 Android 项目中遇到的问题。
如何在实际项目中使用 RxJava?
在我们使用 RxJava 和 RxKotlin 进行反应式编程的文章中,我们创建了一些简单的Observables
并将Observers
数据打印到 Android Studio 的Logcat ——但这不是您在现实世界中使用 RxJava 的方式。
在本文中,我将向您展示如何使用 RxJava 创建一个在无数 Android 应用程序中使用的屏幕:经典的注册屏幕。
如果您的应用有 任何 类型的注册体验,那么它通常会对它接受的信息类型有严格的规定。例如,密码可能需要超过一定数量的字符,或者电子邮件地址必须采用有效的电子邮件格式。
虽然您可以在用户点击“注册”按钮后检查用户的输入 ,但这并不是最好的用户体验,因为它让他们可以提交显然永远不会被您的应用程序接受的信息。
最好在用户输入时对其进行监控,然后在发现他们输入的信息不符合您的应用程序要求时立即提醒他们。通过提供这种实时和持续的反馈,您可以让用户有机会 在 点击 注册 按钮之前纠正他们的错误。
虽然您 可以 使用 vanilla Kotlin 监控用户活动,但我们可以通过获得 RxJava 以及一些其他相关库的帮助,使用更少的代码来提供此功能。
创建用户界面
让我们从构建我们的用户界面开始。我将添加以下内容:
二
EditTexts
,用户可以在其中输入他们的电子邮件地址(enterEmail
)和密码(enterPassword
)。两个
TextInputLayout
包装器,它们将包围我们的enterEmail
andenterPassword
EditTexts
。每当用户输入不符合我们应用要求的电子邮件地址或密码时,这些包装器都会显示警告。密码可见性按钮,允许用户在隐藏密码和以纯文本形式查看密码之间切换。
一个 注册 按钮。为了帮助这个例子专注于 RxJava,我不会实现注册体验的这一部分,所以我会将此按钮标记为禁用。
这是我完成的布局:
<android.support.constraint.ConstraintLayout xmlns:android="https://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:orientation="vertical"> <TextView android:id="@+id/signUp" android:layout_width="wrap_content" android:layout_height="34dp" android:layout_marginTop="16dp" android:gravity="center" android:text="Sign up for an account" android:textColor="#D81B60" android:textSize="25sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.design.widget.TextInputLayout android:id="@+id/emailError" android:layout_width="match_parent" android:layout_height="81dp" app:layout_constraintBottom_toTopOf="@+id/passwordError" app:layout_constraintTop_toBottomOf="@+id/signUp" app:layout_constraintVertical_bias="0.100000024" app:layout_constraintVertical_chainStyle="packed" tools:layout_editor_absoluteX="0dp"> <EditText android:id="@+id/enterEmail" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Email address" android:inputType="textEmailAddress" /> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:id="@+id/passwordError" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginEnd="10dp" app:layout_constraintBottom_toTopOf="@+id/buttonSignUp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/emailError" app:passwordToggleEnabled="true"> <EditText android:id="@+id/enterPassword" android:layout_width="392dp" android:layout_height="wrap_content" android:hint="Create your password" android:inputType="textPassword" /> </android.support.design.widget.TextInputLayout> <Button android:id="@+id/buttonSignUp" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#0000FF" android:enabled="false" android:text="Sign Up" android:textColor="@android:color/white" app:layout_constraintBottom_toBottomOf="parent" /> </android.support.constraint.ConstraintLayout>
如果需要,您可以将其复制/粘贴到您的应用程序中,或者您可以从我们的 GitHub 存储库下载项目源代码。
使用 Kotlin 创建反应式登录体验
现在让我们看看如何使用 RxJava 以及一些相关的库来实时监控用户输入并提供反馈。
我将分两部分处理注册屏幕。在第一部分中,我将向您展示如何使用 RxBinding 库来注册和响应文本更改事件。在第二部分中,我们将创建一些验证用户输入的转换函数,然后在适当的地方显示错误消息。
使用您选择的设置创建一个新项目,但在出现提示时,请确保您选中了Include Kotlin Support复选框。
响应文本更改事件
在本节中,我们将实现以下功能:
检测用户何时在
enterEmail
字段中输入。忽略在短时间内发生的所有文本更改事件,因为这表明用户仍在键入。
当用户停止输入时执行操作。在我们完成的应用程序中,我们将在这里验证用户的输入,但在本节中,我将只显示一个
toast
.
1. 接收绑定
RxBinding 是一个库,可以更轻松地将各种 UI 事件转换为 Observable,此时您可以像对待任何其他 RxJava 数据流一样对待它们。
我们将通过将 RxBindingwidget.RxTextView
与afterTextChangeevents
方法结合来监控文本更改事件,例如:
RxTextView.afterTextChangeEvents(enterEmail)
将文本更改事件视为数据流的问题在于,最初enterEmail
和enterPassword EditTexts
都是空的,我们不希望我们的应用程序对这种空状态做出反应,就好像它是流中的第一个数据发射一样。RxBinding 通过提供一种skipInitialValue()
方法来解决这个问题,我们将使用该方法来指示每个观察者忽略其流的初始值。
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue ()
我在我的RxJava 2 for Android Apps文章中更详细地查看了 RxBinding 库。
2. RxJava 的.debounce()
运算符
为了提供最佳的用户体验,我们需要在用户完成输入之后、但在他们点击注册按钮之前显示任何相关的密码或电子邮件警告。
如果没有 RxJava,识别这个狭窄的时间窗口通常需要我们实现 a Timer
,但在 RxJava 中,我们只需要将debounce()
运算符应用于我们的数据流。
我将使用debounce()
运算符过滤掉所有快速连续发生的文本更改事件,即当用户仍在键入时。在这里,我们忽略了在同一个 400 毫秒窗口内发生的所有文本更改事件:
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue () .debounce(400, TimeUnit.MILLISECONDS)
3.RxAndroid的AndroidSchedulers.mainThread()
RxAndroid 库AndroidSchedulers.mainThread
为我们提供了一种简单的方法来切换到 Android 最重要的主 UI 线程。
由于只能从主 UI 线程更新 Android 的 UI,因此我们需要确保在尝试显示任何电子邮件或密码警告之前以及在显示我们的Toast
.
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue () .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread())
4.订阅
要接收由 发出的数据enterEmail
,我们需要订阅它:
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe {
5.显示吐司
最终,我们希望我们的应用程序通过验证用户的输入来响应文本更改事件,但为了帮助保持简单,此时我将简单地显示一个Toast
.
您的代码应如下所示:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.Toast import com.jakewharton.rxbinding2.widget.RxTextView import kotlinx.android.synthetic.main.activity_main.* import io.reactivex.android.schedulers.AndroidSchedulers import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { Toast.makeText(this, "400 milliseconds since last text change", Toast.LENGTH_SHORT).show() } } }
6. 更新你的依赖
由于我们使用了几个不同的库,我们需要打开项目的build.gradle文件并添加 RxJava、RxBinding 和 RxAndroid 作为项目依赖项:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:design:28.0.0-alpha1' implementation 'com.android.support:appcompat-v7:28.0.0-alpha1' implementation 'com.android.support.constraint:constraint-layout:1.1.0' //Add the RxJava dependency// implementation 'io.reactivex.rxjava2:rxjava:2.1.9' //Add the RxAndroid dependency// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' //Add the RxBinding dependency// implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' }
您可以通过将其安装在您的物理 Android 智能手机或平板电脑或 Android 虚拟设备 (AVD) 上来测试您项目的这一部分。选择enterEmail
EditText
并开始输入;aToast
应该在您停止输入时出现。
使用转换函数验证用户的输入
接下来,我们需要制定一些关于我们的应用程序将接受的输入类型的基本规则,然后根据此标准检查用户的输入并在适当的地方显示错误消息。
检查用户的电子邮件或密码是一个多步骤的过程,因此为了使我们的代码更易于阅读,我将所有这些步骤组合到它们自己的转换函数中。
这是validateEmail
转换功能的开始:
//Define an Observabletransformer. Input and output must be a string// private val validateEmailAddress = ObservableTransformer<String, String> { observable -> //Use flatMap to apply a function to every item emitted by the Observable// observable.flatMap { //Trim any whitespace at the beginning and end of the user’s input// Observable.just(it).map { it.trim() } //Check whether the input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() }
在上面的代码中,我们使用filter()
操作符根据是否匹配 Android 的Patterns.EMAIL_ADDRESS
模式来过滤 Observable 的输出。
在转换函数的下一部分中,我们需要指定如果输入与模式不匹配会发生什么EMAIL_ADDRESS
。默认情况下,每个不可恢复的错误都会触发对 的调用onError()
,从而终止数据流。我们希望我们的应用程序显示一条错误消息,而不是结束流,所以我将使用onErrorResumeNext
,它指示 Observable 通过将控制权传递给新的 Observable 来响应错误,而不是调用onError()
。这允许我们显示我们的自定义错误消息。
//If the user’s input doesn’t match the email pattern, then throw an error// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } }
最后一步是使用.compose()
运算符将此转换函数应用于电子邮件数据流。此时,您的MainActivity.kt应如下所示:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Patterns import io.reactivex.Observable import io.reactivex.ObservableTransformer import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit import com.jakewharton.rxbinding2.widget.RxTextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .map { emailError.error = null it.view().text.toString() } .debounce(400, //Make sure we’re in Android’s main UI thread// TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) .compose(validateEmailAddress) .compose(retryWhenError { passwordError.error = it.message }) .subscribe() } //If the app encounters an error, then try again// private inline fun retryWhenError(crossinline onError: (ex: Throwable) -> Unit): ObservableTransformer<String, String> = ObservableTransformer { observable -> observable.retryWhen { errors -> //Use the flatmap() operator to flatten all emissions into a single Observable// errors.flatMap { onError(it) Observable.just("") } } } //Define an ObservableTransformer, where we’ll perform the email validation// private val validateEmailAddress = ObservableTransformer<String, String> { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Check whether the user input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() } //If the user’s input doesn’t match the email pattern, then throw an error// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } } }
在您的 Android 设备或 AVD 上安装此项目,您会发现注册屏幕的电子邮件部分现在正在成功检查您的输入。尝试输入电子邮件地址以外的任何内容,应用程序会警告您这不是有效的输入。
冲洗并重复:检查用户密码
在这一点上,我们有一个功能齐全enterEmail
的领域——实施enterPassword
主要只是重复相同步骤的情况。
事实上,唯一的主要区别是我们的validatePassword
转换函数需要检查不同的标准。我将指定用户的密码输入必须至少有 7 个字符长:
.filter { it.length > 7 }
重复上述所有步骤后,完成的MainActivity.kt应如下所示:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Patterns import io.reactivex.Observable import io.reactivex.ObservableTransformer import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit import com.jakewharton.rxbinding2.widget.RxTextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //Respond to text change events in enterEmail// RxTextView.afterTextChangeEvents(enterEmail) //Skip enterEmail’s initial, empty state// .skipInitialValue() //Transform the data being emitted// .map { emailError.error = null //Convert the user input to a String// it.view().text.toString() } //Ignore all emissions that occur within a 400 milliseconds timespan// .debounce(400, //Make sure we’re in Android’s main UI thread// TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) //Apply the validateEmailAddress transformation function// .compose(validateEmailAddress) //Apply the retryWhenError transformation function// .compose(retryWhenError { emailError.error = it.message }) .subscribe() //Rinse and repeat for the enterPassword EditText// RxTextView.afterTextChangeEvents(enterPassword) .skipInitialValue() .map { passwordError.error = null it.view().text.toString() } .debounce(400, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) .compose(validatePassword) .compose(retryWhenError { passwordError.error = it.message }) .subscribe() } //If the app encounters an error, then try again// private inline fun retryWhenError(crossinline onError: (ex: Throwable) -> Unit): ObservableTransformer<String, String> = ObservableTransformer { observable -> observable.retryWhen { errors -> ///Use the flatmap() operator to flatten all emissions into a single Observable// errors.flatMap { onError(it) Observable.just("") } } } //Define our ObservableTransformer and specify that the input and output must be a string// private val validatePassword = ObservableTransformer<String, String> { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Only allow passwords that are at least 7 characters long// .filter { it.length > 7 } //If the password is less than 7 characters, then throw an error// .singleOrError() //If an error occurs.....// .onErrorResumeNext { if (it is NoSuchElementException) { //Display the following message in the passwordError TextInputLayout// Single.error(Exception("Your password must be 7 characters or more")) } else { Single.error(it) } } .toObservable() } } //Define an ObservableTransformer, where we’ll perform the email validation// private val validateEmailAddress = ObservableTransformer<String, String> { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Check whether the user input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() } //If the user’s input doesn’t match the email pattern...// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { ////Display the following message in the emailError TextInputLayout// Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } } }
enterEmail
在您的 Android 设备或 AVD 上安装此项目,并尝试在和enterPassword
字段中键入内容。如果您输入的值不符合应用程序的要求,它将显示相应的警告消息,而无需您点击注册按钮。
您可以从 GitHub 下载这个完整的项目。
结论
在本文中,我们通过使用 RxJava 2.0、RxBinding 和 RxAndroid 创建注册屏幕,了解了 RxJava 如何帮助解决您在开发自己的 Android 应用程序时遇到的实际问题。
- 创建用户界面
- 响应文本更改事件
- 1. 接收绑定
- 2. RxJava 的.debounce()运算符
- 3.RxAndroid的AndroidSchedulers.mainThread()
- 4.订阅
- 5.显示吐司
- 6. 更新你的依赖
- 使用转换函数验证用户的输入
- 冲洗并重复:检查用户密码