Androidで登録バリデーションフォームをサクッと作る

Androidの今風のスタックでバリデーションをどう作るのかサクッと実装してみた

よくあるEメールとパスワードを入力するものを想定しています
<img width="368" alt="スクリーンショット 2020-01-28 22.34.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260353/1a939355-659b-1412-644d-77463eaa21c7.png">

今回はEメールとパスワードを打ち込み、条件を満たせばREGISTERボタンが活性化するような仕組みとします

# 環境
* Kotlin
* AndroidX
* Databinding(2way)
* MVVM
* Gradle

一部抜粋

```build.gradle
// LifeCycle
def lifecycle_version = "2.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
```

# 実装

## ViewModel

```EmailRegisterViewModel.kt
class EmailRegisterViewModel : ViewModel() {

    val email = MutableLiveData<String>()
    val password = MutableLiveData<String>()

    private val _buttonEnabledState = MediatorLiveData<Boolean>()
    val buttonEnabledState: LiveData<Boolean>
        get() = _buttonEnabledState

    init {
        _buttonEnabledState.addSource(email) { validateLogin() }
        _buttonEnabledState.addSource(password) { validateLogin() }
    }

    private fun validateLogin() {
        _buttonEnabledState.value = validateEmail() && validatePassword()
    }

    private fun validateEmail() = Patterns.EMAIL_ADDRESS.matcher(email.value ?: "").matches()

    private fun validatePassword(): Boolean {
        return Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&*])(?=\\S+$).{8,}$")
            .matcher(password.value ?: "").matches()
    }

    fun registerButtonClicked(context: Context) {
        val email = email.value ?: return
        val password = password.value ?: return
        Toast.makeText(context, "email: $email, password: $password", Toast.LENGTH_SHORT).show()
    }

    override fun onCleared() {
        super.onCleared()
        _buttonEnabledState.removeSource(email)
        _buttonEnabledState.removeSource(password)
    }
}
```

ここではDatabindingする`email`と`password`を`MutableLiveData`としておき、その2つを対象とした結果をあわせてvalidationをするための観測対象として

`_buttonEnabledState = MediatorLiveData<Boolean>()`

を設定しています　これにより、Eメール側のバリデーションがあっていて、パスワード側のバリデーションをあっていれば`true`、そうでなければ`false`とするようなLiveDataを作り出すことができます

そうするためにはMutableLiveDataに観測したいLiveDataを登録する必要があるため、

```kotlin
init {
    _buttonEnabledState.addSource(email) { validateLogin() }
    _buttonEnabledState.addSource(password) { validateLogin() }
}
```

このように、対象の観測したい`LiveData`とその観測対象に関連するオブザーバーを追加しておきます
例えばEmailの値が変わった際に`validateLogin`を実行して都度判定がされるということになります

ここでの`validateLogin`は

```kotlin
private fun validateLogin() {
    _buttonEnabledState.value = validateEmail() && validatePassword()
}
```

としており、先程の`MediatorLiveData`の値をEmailの条件パターンに合致するか(=`validateEmail`)とPasswordの条件パターンに合致するか(=`validatePassword`)の真偽値をかけ合わせたものとすることで実現できます
ここで`_buttonEnabledState`が`true`になれば両条件を満たしたということになります

あとはvalidateEmail及びvalidatePasswordに皆さんが実装したいロジックをかけば実現できます
今回はEmailに関してはAndroidでは既存で存在する[Patterns#EMAIL_ADDRESS](https://developer.android.com/reference/android/util/Patterns#EMAIL_ADDRESS)を使うことで独自でロジックを書くことなくEメールの判定をしてくれるメソッドが書きやすく実現できました

パスワードに関しては月並みですが特定の記号・数字・小文字・大文字を含んだ8文字以上が条件となるように
regexを
`"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&*])(?=\\S+$).{8,}$"`
と定義して、それに合うかどうかを確認しました

さいごにViewModelがdestroyされた際にリソースを解放するため
`onCleard`にて`MediatorLiveData#remove`を実行しています

## XML

```activity_email_register.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="viewModel"
            type="com.example.chatappsandbox.ui.register.EmailRegisterViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:id="@+id/emajl"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="enter email"
            android:text="@={viewModel.email}" />

        <EditText
            android:id="@+id/password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="enter password"
            android:text="@={viewModel.password}" />

        <Button
            android:id="@+id/register"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:enabled="@{viewModel.buttonEnabledState}"
            android:text="Register" />

    </LinearLayout>
</layout>
```
XMLではDatabindingを使います
対象となるViewModelを`<variable>`として設定し、
email及びpasswordを入力するEdittext部分に使います

ここで注意したいのは今回のようなEdittextに入力かつその値を参照する場合、単方向ではなく双方向のデータバインディングを使う必要があるため、

`android:text="@={viewModel.email}"`

このように宣言の仕方が`@={}`の型をとります　たまに=をつけ忘れることが多いので注意が必要です

[参考: 双方向データバインディング](https://developer.android.com/topic/libraries/data-binding/two-way?hl=ja)

## Activity(=View)
本来であればFragmentに書いていくのがきれいなのですが、簡略化のためにActivityにべたがきします

```EmailRegisterActivity.kt
class EmailRegisterActivity : AppCompatActivity() {

    private val emailRegisterViewModel: EmailRegisterViewModel by lazy {
        ViewModelProvider.NewInstanceFactory().create(EmailRegisterViewModel::class.java)
    }

    private lateinit var activityLoginBinding: ActivityEmailRegisterBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activityLoginBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_email_register)
        activityLoginBinding.also {
            it.viewModel = emailRegisterViewModel
            it.lifecycleOwner = this
        }

        emailRegisterViewModel.buttonEnabledState.observe(this) {
            activityLoginBinding.register.isEnabled = it
        }
        activityLoginBinding.register.setOnClickListener {
            emailRegisterViewModel.registerButtonClicked(this)
        }
    }
}
```

Databindingをしている時点で`<xmlの名前>Binding`が作成されているのでそれを使いviewModelとDatabindingとView(=Activity)が機能するようにしていきます

```kotlin
emailRegisterViewModel.buttonEnabledState.observe(this) {
    activityLoginBinding.register.isEnabled = it
}
```

にて、観測対象のbuttonEnabledStateに対して登録ボタンのisEnabledとMediatorLiveData<Boolean>との値を紐付けることによってボタンが条件で発火するようになります

これで実装面は終わりです！

ためしに確かめてみると

この状態ではパスワードに大文字が含まれていないので非活性状態のままですが
<img width="371" alt="スクリーンショット 2020-01-28 23.13.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260353/2c609a19-b55b-cdc3-ca9d-c154794c5849.png">

大文字を入れると
<img width="372" alt="スクリーンショット 2020-01-28 23.15.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260353/239cd4ea-ba4d-e16f-a6cb-95d54a3d5787.png">
活性状態になってくれました　あとはボタンを押したあとのロジックはお好きなものを実装すればよいかと思います