[Android] DataBinding/ViewBinding をシンプル・安全に利用する拡張関数
はじめに
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
拡張関数
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.view
が null
となるので 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
拡張関数
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