【Android】Fragmentで安全にBindingObjectを参照する

![Last Modified-2020/08/27](https://img.shields.io/badge/Last Modified-2020/08/27-brightgreen)
※ @zizi4n5 さんの[こちら](https://qiita.com/zizi4n5/items/c5f0013236fd055c3e02)の記事を参考にラベルを付けてみました:pray:

どうもこんにちは、いつも便利なDelegateでハッピーな開発をしたい うえむーです。

親のFragmentのコンストラクタにLayoutリソースを引数としてとれるようになって、viewの生成を自前でやる必要がなくなりましたね！
しかし、onCreateViewをoverrideしなくてよくなったのは良くも悪くも何かしらの影響があるというもの。
例えばこのような課題がありました...

> 今までonCreateViewで生成してFragmentにキャッシュしていたbindingObjectを、既存のコードを極力変更せずにどうやって取得しよう...:thinking: 

→ Delegateで取得できそう？

> viewが破棄された後（onDestroyView後）でもbindingObjectを参照できちゃってnullで落ちる...:cry:

→ viewが存在する場合のみ、bindingObjectを参照できるようにする。

これらをスマートに解決できたので紹介します。

## How to use
Delegateなので `by bindings<T : ViewDataBinding>` のように使います。
`T` には、このDeletageを使うFragmentのLayoutから自動生成されたBindingクラスを指定します。
ブロック内でbindingの設定をするという特徴から、今回はuseBindingと命名しました。

```SomeFragment.kt
class SomeFragment : Fragment(R.layout.fragment_some) {
    
    // bindingObjectを取得
    private val useBinding by bindings<FragmentSomeBinding>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // onViewCreatedではviewがnullでないので、useBindingのブロックが実行される
        useBinding { fragmentSomeBinding ->
            // bindingの設定
            fragmentSomeBinding.someTextView.text = "Hello, binding"
            fragmentSomeBinding.someButton.setOnClickListener {
                // ...
            } 
        }
    }
}
```

## Program
DelegateのgetValueはbindingObjectをブロック内で参照できるような `UseBinding<Binding : ViewDataBinding>` を返します。bindingObjectがnullでない場合にのみブロック内の処理が実行されるようになっています。

```FragmentExt.kt
// by bindings として使えるようにする
fun <T : ViewDataBinding> bindings() = UseBindingHelper<T>()

class UseBindingHelper<T : ViewDataBinding> {
    // Delegate！
    operator fun getValue(fragment: Fragment, prop: Any?): UseBinding<T> {
        // viewがnullでない場合にのみ、bindingObjectの取得をする　　　　
        val binding = fragment.view
            ?.setUpBinding(fragment.viewLifecycleOwner)
        return UseBinding(binding)
    }

    // viewからbindingObjectを取得
    private fun View.setUpBinding(lifecycleOwner: LifecycleOwner): T? {
        return DataBindingUtil.bind<T>(this)
            // lifecycleOwnerの設定忘れを防止できる
            ?.also { it.lifecycleOwner = lifecycleOwner }
    }
}

class UseBinding<Binding : ViewDataBinding>(
    val binding: Binding?
) {
    inline operator fun invoke(
        block: (binding: Binding) -> Unit
    ) {
        // bindingがnullでない場合にのみ、blockを実行するので安全
        binding?.let {
            block(it)
        }
    }
}
```

<details><summary>その他の関連コード</summary><div>

```fragment_some.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Bindingクラスを自動生成するので、layoutタグで囲むのを忘れずに -->
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <TextView
            android:id="@+id/some_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
       　　　app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />
        <Button
            android:id="@+id/some_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/some_text_view"
            />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

</div></details>

## Conclude
viewが破棄された後にbindingObjectを参照してnullポになるのは、ちょっと考えれば気づくと思います。
しかし、アニメーションなどはonDestroyViewの後でも動きっぱなしの場合がありえるため、アニメーションのコールバック中でbindingObjectを参照していると意図しないタイミングでエラーにってしまうのは、なかなかピンと来ない部分かと思います。
今回紹介した方法を使えばその辺りをうまくカバーしてくれるので、仮にアニメーションを使っていないとしても、意図しないタイミングでのbindingObjectの参照が発生した時のバグ修正のコストを削減できると思います！

## Reference
- [Qiita記事にさりげなく（けど、わかりやすく）環境を記載する](https://qiita.com/zizi4n5/items/c5f0013236fd055c3e02)
- [生成されたバインディング クラス](https://developer.android.com/topic/libraries/data-binding/generated-binding?hl=ja)