ViewModel + LiveData + Databindingでボタンの活性制御

# 今回の仕様
メールアドレス = 入力されている
パスワード = ６文字以上

この２つが満たされているときだけログインボタンが押せるような、よくあるログイン画面を作りたいと思います。

こんな画面を想定
![qiita_ad2019.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14421/6596a5b0-fac7-677d-6d81-eace13c55928.gif)

# 環境
Android Studio 3.5.2
その他はbuild.gradle参照してください。

# 必要な知識
- Android Studioを使える
- kotlinを書ける
- activityとfragmentを使ったことがある

# リポジトリ

[iwahara/button\_enabled\_sample: for Advent Calendar 2019](https://github.com/iwahara/button_enabled_sample)


# はじめに

今回はAndroidXを使うので、プロジェクトを作る際にそれを有効化します。
Fragment+ViewModelを選ぶとはじめから画面表示まで揃ってるのでやりやすいです。
プロジェクトの設定でAndroidXを使う設定を忘れずに。
<img width="1012" alt="configure_project.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14421/ec827b58-233d-3cca-0e40-9e7abb1633ab.png">

## app/build.gradle
DataBindingを使うので有効化します。
`kotlin-kapt`プラグインとdataBindingの有効化を設定します。

```gradle
//追加
apply plugin: 'kotlin-kapt'

android {

    //追加
    dataBinding {
        enabled = true
    }
}
```

また、依存ライブラリは以下の通りです。

```gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
    implementation 'com.google.android.material:material:1.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
```


## MainViewModelクラス
MainFragmentのViewModelです。今回の肝となるクラスです。
表示に必要なロジックを書くクラスになります。
詳しい解説はあとにして、まずはクラス全体のコードです。

```kotlin:全体
package com.sample.buttonenabled.ui.main

import androidx.lifecycle.*

class MainViewModel : ViewModel() {

    val mailAddress = MutableLiveData<String>("")
    val password = MutableLiveData<String>("")
    val isSaveMailAddress = MutableLiveData<Boolean>(false)

    private val _isButtonLoginEnabled = MediatorLiveData<Boolean>()

    val isButtonLoginEnabled: LiveData<Boolean> = _isButtonLoginEnabled

    init {

        val observer = Observer<String> {
            val mail = this.mailAddress.value ?: ""
            val password = this.password.value ?: ""
            this._isButtonLoginEnabled.value = mail.isNotEmpty() && password.trim().length >= 6
        }
        _isButtonLoginEnabled.addSource(mailAddress, observer)
        _isButtonLoginEnabled.addSource(password, observer)
    }
}

```

### 解説

#### Observableな変数を定義
ここで各画面変数を`LiveData`として宣言します。
`LiveData`を使うことで、画面にて入力があった際に自動的にこれらの変数に値が格納されるようになります(別途layoutでの設定が必要になりますが)。

基本的には`MutableLiveData`ですが、最後の`isButtonLoginEnabled`だけ`MediatorLiveData`なので注意！

```kotlin

    val mailAddress = MutableLiveData<String>("")
    val password = MutableLiveData<String>("")
    val isSaveMailAddress = MutableLiveData<Boolean>(false)

    val isButtonLoginEnabled = MediatorLiveData<Boolean>()
```


#### initでMediatorLiveDataの設定を行う

MediatorLiveDataは複数のLiveDataをもとに値を更新したいときに使います。
今回であれば、メールアドレスとパスワードをもとに、ログインボタンの活性制御をしたい感じですね。
そのためには、まず`androidx.lifecycle.Observer`を使って動作を定義し、`isButtonLoginEnabled`に監視したい値とともに設定する必要があります。
なお、Observerの型パラメータは監視されるLiveDataの型パラメータと一致する必要があります。
今回は両方ともStringだったので同じObserverを使いまわしていますが、もし違う場合はObserverを別で定義する必要があります。

本題からは外れますが、MediatorLiveDataをpublicにはせず、LiveDataとして露出しています。
これは、ViewModel内のデータでのみ更新可能にするためです。

```kotlin
init {

        val observer = Observer<String> {
            val mail = this.mailAddress.value ?: ""
            val password = this.password.value ?: ""
            this._isButtonLoginEnabled.value = mail.isNotEmpty() && password.trim().length >= 6
        }
        _isButtonLoginEnabled.addSource(mailAddress, observer)
        _isButtonLoginEnabled.addSource(password, observer)
    }
```

## layout/main_fragment.xml

ViewModelをDataBindingして使うための設定が必要となります。
こちらも詳しい解説はあとにして、まずはコード全体です。

```xml:全体
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="viewmodel"
            type="com.sample.buttonenabled.ui.main.MainViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.login.LoginFragment">


        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <EditText
                android:id="@+id/edit_email"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="メールアドレス"
                android:text="@={viewmodel.mailAddress}"
                android:inputType="textEmailAddress"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/layout_mail_address">

            <EditText
                android:id="@+id/edit_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="パスワード"
                android:inputType="textPassword"
                android:text="@={viewmodel.password}"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>

        <androidx.appcompat.widget.AppCompatCheckBox
            android:id="@+id/check_save_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="メールアドレスを保存する"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/layout_password"
            android:checked="@={viewmodel.saveMailAddress}"
            />

        <Button
            android:id="@+id/button_login"
            android:enabled="@{viewmodel.isButtonLoginEnabled}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="ログイン"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/check_save_mail_address" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

### 解説

#### 画面で使うViewModelの定義
まずは、ルート要素を`layout`タグにし、その中で`data`と`ConstraintLayout`を定義するようにします。
dataの子要素としてvariableを定義し、先程作った`MainViewModel`を指定します。
nameは何でも良いのですが、ここではわかりやすく`viewmodel`としています。

```xml
<layout 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">

    <data>
        <variable
            name="viewmodel"
            type="com.sample.buttonenabled.ui.main.MainViewModel" />
    </data>
```


#### 画面項目にViewModelの該当するLiveDataをBindする
`android:text="@={viewmodel.mailAddress}"`でviewModelのMailAdressをbindしています。
これで、メールアドレスの入力欄に何か入力されたら自動でViewModelにも設定されるようになります。
パスワードも同様に設定します。

```xml

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <EditText
                android:id="@+id/edit_email"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="メールアドレス"
                android:text="@={viewmodel.mailAddress}"
                android:inputType="textEmailAddress"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>
```

#### Buttonの活性制御にViewModelのisButtonLoginEnabledをBindする
`android:enabled="@{viewmodel.isButtonLoginEnabled}"`をenabledにbindすることで、活性制御を自動で行うようにします。

```xml

        <Button
            android:id="@+id/button_login"
            android:enabled="@{viewmodel.isButtonLoginEnabled}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="ログイン"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/check_save_mail_address" />
```


## MainFragmentクラス
ViewModelを作って保持し、ログインボタンを押したときの処理を書きます。
ロジックはViewModelに持っているので、ほぼ表示とイベント設定に関することのみ書きます。

詳しい解説はあとにして、まずはクラス全体のコードです。

```kotlin:全体
package com.sample.buttonenabled.ui.main

import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import com.sample.buttonenabled.R
import com.sample.buttonenabled.databinding.MainFragmentBinding
import kotlinx.android.synthetic.main.main_fragment.*

class MainFragment : Fragment() {
    private lateinit var binding: MainFragmentBinding

    companion object {
        fun newInstance() = MainFragment()
    }

    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this).get(MainViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewmodel = this.viewModel
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)


        this.button_login.setOnClickListener {
            val email = this.viewModel.mailAddress.value
            val pass = this.viewModel.password.value
            Toast.makeText(requireContext(), "${email} : ${pass}", Toast.LENGTH_SHORT).show()
        }
    }

}

```


### 解説

#### ViewModelの取得

ViewModelは普通にインスタンス化するのではなく、ViewModelProvidersから取得するようにします。

```kotlin

    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this).get(MainViewModel::class.java)
    }
```

なお、2.2.0からは`ViewModelProviders.of`はdeprecatedになります。
推奨される方法は以下の記事を参考にしてください。

[Android Jetpack\(AndroidXライブラリ\)の最近の更新 \- Qiita](https://qiita.com/FKbelm/items/4824fcf8e7eb7c879f81#viewmodel%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96)


#### DataBindingの設定
`MainFragmentBinding` は事前にリビルドしないと出てこないので、一度リビルドしましょう。
`inflater`の代わりに`DataBindingUtil.inflate`を使います。
その際にlifecycleOnwerとviewmodelを設定します。こうすると、ViewModelのライフサイクルが適切に処理されるようになります。

```kotlin
    private lateinit var binding: MainFragmentBinding


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewmodel = this.viewModel
        return binding.root
    }

```

# 実際に動かす

ここまで来たら動くようになっているはずです。
ここまでボイラープレートをなくすことが出来るとは、便利な世の中になりましたね。