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应用程序中使用的屏幕:经典的Sign Up屏幕。
如果您的应用具有任何类型的注册经验,则通常会对接收的信息有严格的规定。 例如,可能密码需要超过一定数量的字符,或者电子邮件地址必须采用有效的电子邮件格式。
尽管您可以在用户单击“ 注册”按钮后检查他们的输入,但这并不是最佳的用户体验,因为它使他们可以提交显然不会被您的应用程序接受的信息。
最好在用户输入时监视用户,然后在他们输入的信息不符合您的应用程序要求时立即提示他们。 通过提供这种实时和持续的反馈,您可以使用户有机会在点击“ 注册”按钮之前纠正他们的错误。
虽然您可以使用Vanilla Kotlin监视用户活动,但我们可以通过使用RxJava以及其他一些相关库的帮助,以更少的代码来提供此功能。
创建用户界面
让我们从构建用户界面开始。 我将添加以下内容:
- 两个
EditTexts
,用户可以在其中输入其电子邮件地址(enterEmail
)和密码(enterPassword
)。 - 两个
TextInputLayout
包装器,它们将包围我们的enterEmail
和enterPassword
EditTexts
。 只要用户输入的电子邮件地址或密码不符合我们应用的要求,这些包装器就会显示警告。 - 密码可见性按钮,使用户可以在掩盖密码和以纯文本形式查看之间切换。
- 一个注册按钮。 为了使该示例始终专注于RxJava,我将不会实现注册体验的这一部分,因此将此按钮标记为禁用。
这是我完成的布局:
<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: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库注册和响应文本更改事件。 在第二部分中,我们将创建一些转换函数来验证用户的输入,然后在适当的位置显示错误消息。
使用您选择的设置创建一个新项目,但是在出现提示时,请确保选中“ 包括Kotlin支持”复选框。
响应文本更改事件
在本节中,我们将实现以下功能:
- 检测用户何时在
enterEmail
字段中键入。 - 忽略所有在短时间内发生的文本更改事件,因为这表明用户仍在键入。
- 当用户停止键入时,请执行操作。 在完成的应用程序中,我们将在此验证用户的输入,但是在本节中,我将仅显示
Toast
。
1. RxBinding
RxBinding是一个库,可以轻松地将各种UI事件转换为Observables,在这一点上,您可以像对待任何其他RxJava数据流一样对待它们。
我们将通过结合RxBinding的widget.RxTextView
和afterTextChangeEvents
方法来监视文本更改事件,例如:
RxTextView.afterTextChangeEvents(enterEmail)
将文本更改事件视为数据流的问题在于,最初enterEmail
和enterPassword EditTexts
都将为空,并且我们不希望我们的应用程序对此空状态做出反应,就好像它是流中的第一个数据发射一样。 RxBinding通过提供skipInitialValue()
方法解决了这个问题,我们将使用该方法来指示每个Observer忽略其流的初始值。
RxTextView.afterTextChangeEvents(enterEmail)
.skipInitialValue()
我会在我的RxJava 2 for Android Apps文章中更详细地介绍RxBinding库。
2. RxJava的.debounce()
运算符
为了提供最佳的用户体验,我们需要在用户完成键入之后但在单击“ 注册”按钮之前显示所有相关的密码或电子邮件警告。
如果没有RxJava,识别出狭窄的时间范围通常将需要我们实现一个Timer
,但是在RxJava中,我们只需要将debounce 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
并开始输入; 当您停止输入时,应显示Toast
。
使用转换函数验证用户输入
接下来,我们需要制定一些关于应用程序将接受的输入类型的基本规则,然后根据此标准检查用户的输入并在适当的地方显示错误消息。
检查用户的电子邮件或密码是一个多步骤的过程,因此,为了使我们的代码更易于阅读,我将把所有这些步骤组合到自己的转换函数中。
这是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()
运算符基于Observable的输出是否匹配Android的Patterns.EMAIL_ADDRESS
模式来对其进行过滤。
在转换功能的下一部分中,我们需要指定如果输入与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()
}
}
}
在您的Android设备或AVD上安装此项目,然后尝试在enterEmail
和enterPassword
字段中键入enterPassword
。 如果您输入的值不符合应用程序的要求,那么它将显示相应的警告消息, 而无需点击“ 注册”按钮。
您可以从GitHub下载此完整项目 。
结论
在本文中,我们研究了RxJava如何通过使用RxJava 2.0,RxBinding和RxAndroid创建“ 注册”屏幕来帮助解决在开发自己的Android应用程序时遇到的现实问题。
有关RxJava库的更多背景信息,请务必查看我们的RxJava 2.0入门文章。