🤖

[Android] DataBinding/ViewBinding をシンプル・安全に利用する拡張関数

2021/02/18に公開

はじめに

Android アプリ開発で DataBinding/ViewBinding を利用する際に、これらをシンプル・安全に利用できる拡張関数を紹介します。今回、紹介する拡張関数を利用すると、次のメリットがあります。

  • Binding を定義するコードを統一的な方法で、かつ、シンプルに書ける。
  • Fragment の Binding の開放忘れを防止できる。
  • Fragment で onDestroyView() 後に Binding にアクセスすると例外が発生する問題を回避できる。
    • Fragment の Binding を Nullable 型で提供。
  • (DataBinding で LiveData を利用する場合) setLifecycleOwner() の呼び出し忘れを防止できる。

DataBinding-ktx のようなライブラリもありますが Fragment で onDestroyView() 後に Binding にアクセスすると例外が発生します。必ず LiveData の更新を契機に Binding にアクセスする、または、ビューのライフサイクルを意識してコードを書いていれば問題はありませんが、今回の拡張関数は Nullable 型の Binding を提供することで例外の発生を回避しています。

DataBinding

拡張関数

DataBindingExt.kt
package package io.github.hkusu.sample.ext

import android.view.ViewGroup
import androidx.core.view.get
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

fun <T : ViewDataBinding> FragmentActivity.dataBinding(): ReadOnlyProperty<FragmentActivity, T> {
    return object : ReadOnlyProperty<FragmentActivity, T> {
        override fun getValue(thisRef: FragmentActivity, property: KProperty<*>): T {
            val view = thisRef.findViewById<ViewGroup>(android.R.id.content)[0]
            return checkNotNull(DataBindingUtil.bind<T>(view)).apply { lifecycleOwner = thisRef }
        }
    }
}

fun <T : ViewDataBinding> Fragment.dataBinding(): ReadOnlyProperty<Fragment, T?> {
    return object : ReadOnlyProperty<Fragment, T?> {
        override fun getValue(thisRef: Fragment, property: KProperty<*>): T? {
            val view = thisRef.view ?: return null
            return checkNotNull(DataBindingUtil.bind<T>(view)).apply { lifecycleOwner = thisRef }
        }
    }
}

Binding にアクセスする度に getValue() が呼ばれますが、DataBindingUtil がキャッシュを管理しているのでアクセスする度に新しい Binding が生成されることはありません。Fragment の場合、onDestroy() 後は thisRef.viewnull となるので Binding も null となります。

利用例

Activity

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    private val binding: MainActivityBinding by dataBinding()

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

        binding.message.text = "あいうえお"

        // ...

この例ではコンストラクタでレイアウトを渡しているので setContentView() は不要です(setContentView() でビューを生成する場合でもこの拡張関数は利用できます)。super.onCreate(savedInstanceState) より前に Binding にアクセスすると例外が発生するので注意してください。

Fragment

class MainFragment : Fragment(R.layout.main_fragment) {

    private val binding: MainFragmentBinding by dataBinding()

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

        binding?.message?.text = "あいうえお" // Binding は Nullable
	
	// ...

この例ではコンストラクタでレイアウトを渡しているので onCreateView() は不要です(onCreateView() でビューを生成する場合でもこの拡張関数は利用できます)。Fragment のビューが生成される前に Binding にアクセスすると例外が発生するので注意してください。

Fragment の Binding は Nullable 型なので、Null Safety な Kotlin においては安全にアクセスすることができます。この際、Binding を変数等に保存・利用してしまうと意味がないので、Binding にアクセスするコードは常に Fragment のプロパティエリアに by dataBinding() で定義した変数(上述のコードでいうと変数 binding)を利用するようにしてください。

Binding を Nullable として扱うのが煩雑という場合は、onDestoryView() 後にアクセスしないことを別途、保証している必要はありますが、次のようにして NutNull 型の Binding を用意することもできます。

private val _binding: MainFragmentBinding? by dataBinding()
private val binding get() = checkNotNull(_binding)

ViewBinding

拡張関数

ViewBindingExt.kt
package package io.github.hkusu.sample.ext

import android.view.View
import android.view.ViewGroup
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

inline fun <T : ViewBinding> FragmentActivity.viewBinding(crossinline block: View.() -> T): ReadOnlyProperty<FragmentActivity, T> {
    return object : ReadOnlyProperty<FragmentActivity, T> {
        private var binding: T? = null
        override fun getValue(thisRef: FragmentActivity, property: KProperty<*>): T {
            val view = thisRef.findViewById<ViewGroup>(android.R.id.content)[0]
            return binding ?: block.invoke(view).apply { binding = this }
        }
    }
}

inline fun <reified T : ViewBinding> Fragment.viewBinding(crossinline block: View.() -> T): ReadOnlyProperty<Fragment, T?> {
    return object : ReadOnlyProperty<Fragment, T?> {
        override fun getValue(thisRef: Fragment, property: KProperty<*>): T? {
            val view = thisRef.view ?: return null
            val key = T::class.java.name.hashCode()
            @Suppress("UNCHECKED_CAST")
            return view.getTag(key) as? T ?: block.invoke(view).apply { view.setTag(key, this) }
        }
    }
}

ViewBinding の場合は DataBindingUtil のような便利なものは無いので、引数で Binding のファクトリを渡しているのと、Binding のキャッシュを自前で管理しています。Fragment の場合はビューにタグとして Binding を持たせることで、ビューとキャッシュの生存期間を一致させています。

そのほか注意事項等は DataBinding の時と同様です。

利用例

Activity

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    private val binding by viewBinding(MainActivityBinding::bind)

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

        binding.message.text = "あいうえお"

	// ...

Fragment

class MainFragment : Fragment(R.layout.main_fragment) {

    private val binding: MainFragmentBinding? by viewBinding(MainFragmentBinding::bind)

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

        binding?.message?.text = "あいうえお"
	
	// ...

参考にさせていただいた記事

Discussion