![Last Modified-2020/08/27](https://img.shields.io/badge/Last Modified-2020/08/27-brightgreen)
※ @zizi4n5 さんのこちらの記事を参考にラベルを付けてみました
どうもこんにちは、いつも便利なDelegateでハッピーな開発をしたい うえむーです。
親のFragmentのコンストラクタにLayoutリソースを引数としてとれるようになって、viewの生成を自前でやる必要がなくなりましたね!
しかし、onCreateViewをoverrideしなくてよくなったのは良くも悪くも何かしらの影響があるというもの。
例えばこのような課題がありました...
今までonCreateViewで生成してFragmentにキャッシュしていたbindingObjectを、既存のコードを極力変更せずにどうやって取得しよう...
→ Delegateで取得できそう?
viewが破棄された後(onDestroyView後)でもbindingObjectを参照できちゃってnullで落ちる...
→ viewが存在する場合のみ、bindingObjectを参照できるようにする。
これらをスマートに解決できたので紹介します。
How to use
Delegateなので by bindings<T : ViewDataBinding>
のように使います。
T
には、このDeletageを使うFragmentのLayoutから自動生成されたBindingクラスを指定します。
ブロック内でbindingの設定をするという特徴から、今回はuseBindingと命名しました。
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でない場合にのみブロック内の処理が実行されるようになっています。
// 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)
}
}
}
その他の関連コード
<?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>
Conclude
viewが破棄された後にbindingObjectを参照してnullポになるのは、ちょっと考えれば気づくと思います。
しかし、アニメーションなどはonDestroyViewの後でも動きっぱなしの場合がありえるため、アニメーションのコールバック中でbindingObjectを参照していると意図しないタイミングでエラーにってしまうのは、なかなかピンと来ない部分かと思います。
今回紹介した方法を使えばその辺りをうまくカバーしてくれるので、仮にアニメーションを使っていないとしても、意図しないタイミングでのbindingObjectの参照が発生した時のバグ修正のコストを削減できると思います!