DialogFragmentでData Bindingを使う

DialogFragmentのカスタムレイアウトでEditTextの値を取得しようとして苦戦したのが、Data Bindingを使うといい感じに解決できそうだったのでメモ兼ねて記事を残しておきます。
もっと根本的に良い解決方法があれば遠慮なくコメントください。

#今回使用するカスタムレイアウト

```xml:fragment_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content">
    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Dialog"
            android:textSize="20sp" />
    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/edit_text"
            android:hint="入力" />
</LinearLayout>
```
<img width="300" alt="こんな感じ" src="https://qiita-image-store.s3.amazonaws.com/0/270890/3ead78d6-31f4-c0e4-5b66-a8d71aa86815.png">
今回は例として、OKを押すと入力した内容がToastとして表示されるダイアログを考えていきます。
#問題のコード
EditTextの取得にはKotlin Android Extensionsを利用しています。

```kotlin:MyDialogFragment.kt
class MyDialogFragment : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        activity?.let {
            val view = it.layoutInflater.inflate(R.layout.fragment_dialog, null)
            builder.setView(view)
                .setPositiveButton("OK") { _, _->
                    val text = edit_text.text.toString() // ←これだと落ちる
                    Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
                }
        }
        return builder.create()
    }
}
```
一見普通に動きそうですが、`edit_text`の取得で落ちます。

```
java.lang.IllegalStateException: edit_text must not be null
```
本来nullが入らないはずの`edit_text`がnullなようです。

#とりあえずの解決策

`View.findViewById`を使います。

```Kotlin:MyDialogFragment.kt
class MyDialogFragment : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        activity?.let {
            val view = it.layoutInflater.inflate(R.layout.fragment_dialog, null)
            builder.setView(view)
                .setPositiveButton("OK") { _, _->
                    val editText: EditText = view.findViewById(R.id.edit_text) // ←ここでedit_textを取得
                    val text = editText.text.toString()
                    Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
                }
        }
        return builder.create()
    }
}
```

~~一応落ちずに動くのですが、Kotlinを使っているのにも関わらず`findViewById`を駆逐できていないのがなんとも気持ち悪いですよね。~~
View経由でKotlin Android Extensionsを使えば`findViewById`する必要がないようです。

```kotlin
val text = view.edit_text.text.toString()
```

#Data Bindingを使った解決策

Data Bindingについてよく知らない人は、
公式のガイド(https://developer.android.com/topic/libraries/data-binding/?hl=ja)
や
こちらの記事(https://qiita.com/Omoti/items/a83910a990e64f4dbdf1)
を参考にしてください。

まずレイアウトファイルを次のように書き換えます。

```xml:fragment_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content">
        <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Dialog"
                android:textSize="20sp" />
        <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/edit_text"
                android:hint="入力" />
    </LinearLayout>
</layout>
```

レイアウト全体を`layout`タグで囲うことにより、このレイアウトファイルをData Bindingで利用できるようになりました。


```Kotlin:MyDialogFragment.kt
class MyDialogFragment :DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        val binding = DataBindingUtil.inflate<FragmentDialogBinding>(LayoutInflater.from(activity), R.layout.fragment_dialog, null, false)
        val view = binding.root
        builder.setView(view)
            .setPositiveButton("OK") { _, _ ->
                Toast.makeText(activity, binding.editText.text, Toast.LENGTH_SHORT).show()
            }
        return builder.create()
    }
}
```
`FragmentDialogBinding` はプロジェクトをビルドしたときにData Bindingによって自動生成されるクラスです。クラス名はレイアウトファイルのファイル名をアッパーキャメルケースにしたもの+Bindingになります。
Data Binding経由でViewを取得するときには、入力されたidをローワーキャメルケースで指定して取得します。この場合、`edit_text`が`editText`になっていますね。

画面回転などによるActivityの更新などを考慮しない場合、次のように書くこともできます。

```xml:fragment_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable name="text" type="String" />
    </data>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content">
        <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Dialog"
                android:textSize="20sp" />
        <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@={text}"
                android:hint="入力" />
    </LinearLayout>
</layout>
```
```Kotlin:MyDialogFragment.kt
class MyDialogFragment :DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        val binding = DataBindingUtil.inflate<FragmentDialogBinding>(LayoutInflater.from(activity), R.layout.fragment_dialog, null, false)
        val view = binding.root
        builder.setView(view)
            .setPositiveButton("OK") { _, _ ->
                Toast.makeText(activity, binding.text, Toast.LENGTH_SHORT).show()
            }
        return builder.create()
    }
}
```
EditTextにidを設定していないことに気がついたかと思います。`data`タグでレイアウトファイル上に`text`変数を定義し、入力した値を`android:text="@={text}"`とすることによって直接`text`に入れています。Fragment側では`binding.text`とすることで`text`変数を取得することができます。この方法だと画面回転などでActivityが更新されたときに入力中の値は消えてしまいますが、それを考慮する必要がない場合はこちらのほうがよりシンプルです。


#参考
https://developer.android.com/topic/libraries/data-binding/?hl=ja
https://stackoverflow.com/questions/34967868/how-to-use-data-binding-in-dialog