Статьи

Реактивное программирование Kotlin для экрана регистрации Android

RxJava 2.0 — это популярная библиотека реактивного программирования, которая помогла бесчисленным разработчикам Android создавать приложения с высокой скоростью отклика, используя меньше кода и меньше сложности, особенно когда речь идет об управлении несколькими потоками.

Если вы один из многих разработчиков, которые переключились на Kotlin, то это не значит, что вам нужно отказаться от RxJava!

В первой части этой серии я показал вам, как перейти от программирования на RxJava 2.0 в Java к программированию на RxJava в Kotlin. Мы также рассмотрели, как исключить шаблон из ваших проектов, воспользовавшись функциями расширения RxKotlin и секретом решения проблемы преобразования SAM, с которой сталкиваются многие разработчики, когда они впервые начинают использовать RxJava 2.0 с Kotlin.

Во второй части мы сконцентрируемся на том, как RxJava может помочь решить проблемы, с которыми вы сталкиваетесь в реальных проектах Android, путем создания реактивного приложения Android с использованием RxJava 2.0, RxAndroid и RxBinding.

В нашей статье « Реактивное программирование с использованием RxJava и RxKotlin» мы создали несколько простых Observables и Observers которые печатают данные в Logcat в Android Studio, но вы не будете использовать RxJava в реальном мире.

В этой статье я собираюсь показать вам, как использовать RxJava для создания экрана, который используется в бесчисленных приложениях для Android: классический экран регистрации.

Типичный экран регистрации, который вы найдете в бесчисленных приложениях для Android

Если ваше приложение имеет какой- либо опыт регистрации, то оно, как правило, будет иметь строгие правила относительно вида информации, которую оно принимает. Например, возможно, пароль должен превышать определенное количество символов, или адрес электронной почты должен быть в правильном формате электронной почты.

Хотя вы можете проверить ввод пользователя после того, как он нажмет кнопку « Зарегистрироваться» , это не лучший способ взаимодействия с пользователем, поскольку он оставляет их открытыми для отправки информации, которая явно никогда не будет принята вашим приложением.

Гораздо лучше контролировать пользователя, когда он печатает, а затем дать им предупреждение, как только станет ясно, что они вводят информацию, которая не соответствует требованиям вашего приложения. Предоставляя этот вид прямой и постоянной обратной связи, вы даете пользователю возможность исправить свои ошибки, прежде чем нажать на кнопку « Зарегистрироваться» .

В то время как вы можете отслеживать активность пользователей с помощью vanilla Kotlin, мы можем предоставить эту функциональность, используя гораздо меньше кода, используя помощь RxJava, а также несколько других связанных библиотек.

Давайте начнем с создания нашего пользовательского интерфейса. Я собираюсь добавить следующее:

  • Два EditTexts , где пользователь может ввести свой адрес электронной почты ( enterEmail ) и пароль ( enterPassword ).
  • Две обертки TextInputLayout , которые окружают наши enterEmail и enterPassword EditTexts . Эти упаковщики будут отображать предупреждение всякий раз, когда пользователь вводит адрес электронной почты или пароль, которые не соответствуют требованиям нашего приложения.
  • Кнопка видимости пароля, которая позволяет пользователю переключаться между маскировкой пароля и просмотром его в виде простого текста.
  • Кнопка регистрации. Чтобы этот пример был сфокусирован на RxJava, я не буду реализовывать эту часть процесса регистрации, поэтому помечу эту кнопку как отключенную.

Вот мой готовый макет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<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 .

Теперь давайте посмотрим, как мы можем использовать RxJava, а также несколько связанных библиотек, для мониторинга пользовательского ввода и обеспечения обратной связи в режиме реального времени.

Я буду разбираться с экраном регистрации в двух частях. В первом разделе я покажу вам, как использовать библиотеку RxBinding для регистрации и реагирования на события изменения текста. Во втором разделе мы создадим некоторые функции преобразования, которые проверяют вводимые пользователем данные, а затем отобразят сообщение об ошибке, где это необходимо.

Создайте новый проект с настройками по вашему выбору, но при появлении запроса обязательно установите флажок Включить поддержку Kotlin .

В этом разделе мы реализуем следующую функциональность:

  • Определите, когда пользователь печатает в поле enterEmail .
  • Проигнорируйте все события изменения текста, которые происходят в течение короткого промежутка времени, поскольку это указывает, что пользователь все еще печатает.
  • Выполните действие, когда пользователь перестает печатать. В нашем законченном приложении мы будем проверять ввод пользователя, но в этом разделе я просто покажу Toast .

RxBinding — это библиотека, которая упрощает преобразование широкого спектра событий пользовательского интерфейса в Observables, после чего вы можете обрабатывать их так же, как и любой другой поток данных RxJava.

Мы собираемся отслеживать события изменения текста, комбинируя widget.RxTextView в widget.RxTextView с методом afterTextChangeEvents , например:

1
RxTextView.afterTextChangeEvents(enterEmail)

Проблема с обработкой событий изменения текста как потоков данных заключается в том, что изначально и enterEmail и enterPassword EditTexts будут пустыми, и мы не хотим, чтобы наше приложение реагировало на это пустое состояние, как будто это первая enterPassword EditTexts данных в потоке. RxBinding решает эту проблему, предоставляя метод skipInitialValue() , который мы будем использовать для указания каждому наблюдателю игнорировать начальное значение своего потока.

1
2
RxTextView.afterTextChangeEvents(enterEmail)
              .skipInitialValue()

Я смотрю на библиотеку RxBinding более подробно в моей статье о приложениях RxJava 2 для Android .

Чтобы обеспечить наилучшее взаимодействие с пользователем, нам нужно отображать любой соответствующий пароль или предупреждения по электронной почте после того, как пользователь закончил печатать, но до того, как они нажмут кнопку « Зарегистрироваться» .

Без RxJava для идентификации этого узкого промежутка времени обычно требуется реализация Timer , но в RxJava нам просто нужно применить оператор debounce() к нашему потоку данных.

Я собираюсь использовать оператор debounce() чтобы отфильтровать все события изменения текста, которые происходят в быстрой последовательности, т.е. когда пользователь все еще печатает. Здесь мы игнорируем все события изменения текста, которые происходят в одном и том же 400-миллисекундном окне:

1
2
3
RxTextView.afterTextChangeEvents(enterEmail)
              .skipInitialValue()
              .debounce(400, TimeUnit.MILLISECONDS)

AndroidSchedulers.mainThread библиотеки AndroidSchedulers.mainThread нам легко переключаться на важнейший основной поток пользовательского интерфейса Android.

Так как пользовательский интерфейс Android можно обновить только из основного потока пользовательского интерфейса, мы должны убедиться, что мы находимся в этой теме, прежде чем пытаться отображать предупреждения по электронной почте или паролям, а также перед отображением нашего Toast .

1
2
3
4
RxTextView.afterTextChangeEvents(enterEmail)
              .skipInitialValue()
              .debounce(400, TimeUnit.MILLISECONDS)
              .observeOn(AndroidSchedulers.mainThread())

Чтобы получать данные, enterEmail , нам нужно подписаться на них:

1
2
3
4
5
RxTextView.afterTextChangeEvents(enterEmail)
              .skipInitialValue()
              .debounce(400, TimeUnit.MILLISECONDS)
              .observeOn(AndroidSchedulers.mainThread())
              .subscribe {

В конце концов, мы хотим, чтобы наше приложение реагировало на события изменения текста, проверяя вводимые пользователем данные, но, чтобы не усложнять ситуацию, на данный момент я просто собираюсь отобразить Toast .

Ваш код должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()
                    
                    
               }
   }
}

Поскольку мы используем несколько разных библиотек, нам нужно открыть файл build.gradle нашего проекта и добавить RxJava, RxBinding и RxAndroid в качестве зависимостей проекта:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 должен появиться, когда вы перестанете печатать.

Протестируйте свои проекты enterEmail EditText

Затем нам нужно установить некоторые основные правила относительно вида ввода, которое будет принимать наше приложение, а затем проверить ввод пользователя по этим критериям и отобразить сообщение об ошибке, где это необходимо.

Проверка электронной почты или пароля пользователя является многоэтапным процессом, поэтому, чтобы облегчить чтение нашего кода, я собираюсь объединить все эти шаги в их собственную функцию преобразования.

Вот начало функции преобразования validateEmail :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
//Define an ObservableTransformer.
 
   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 поток, мы хотим, чтобы наше приложение отображало сообщение об ошибке, поэтому я собираюсь использовать onErrorResumeNext , который инструктирует Observable реагировать на ошибку, передавая управление новому Observable, а не вызывая onError() . Это позволяет нам отображать наше пользовательское сообщение об ошибке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
//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 должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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 символов:

1
.filter { it.length > 7 }

После повторения всех предыдущих шагов завершенный файл MainActivity.kt должен выглядеть примерно так:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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 и поэкспериментируйте с enterPassword поля enterEmail и enterPassword . Если вы введете значение, которое не соответствует требованиям приложения, оно отобразит соответствующее предупреждение без необходимости нажимать кнопку « Зарегистрироваться» .

Вы можете скачать этот полный проект с GitHub .

В этой статье мы рассмотрели, как RxJava может помочь решить реальные проблемы, с которыми вы столкнетесь при разработке собственных приложений для Android, используя RxJava 2.0, RxBinding и RxAndroid для создания экрана регистрации.

Для получения дополнительной информации о библиотеке RxJava обязательно ознакомьтесь с нашей статьей « Начало работы с RxJava 2.0» .