アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

【Android】ViewBindingの使用から応用

ViewBindingがもうすぐStableになるので使用してみました。ActivityやFragmentの最新版との組み合わせを書いていきます。

公式ドキュメントはこちらになります。
https://developer.android.com/topic/libraries/view-binding

使用準備

build.gradleにViewBindingを使用することを宣言します。この辺りはDataBindingと同じです。

android {
    viewBinding {
        enabled = true
    }
}

自動生成

これによってViewのXML全てのファイル名に対してスネークケースがキャメルケースに変換されて、末尾にBindingと名前が付いたファイルが生成されます。
activity_main.xml -> ActivityMainBinding

生成したくない場合

DataBindingと違ってXMLのRootがlayoutタグで囲われなくてもファイルは生成されます。生成したくないファイルには以下のviewBindingIgnore属性を追加します。

<LinearLayout
        tools:viewBindingIgnore="true">
    
</LinearLayout>

ViewをInflateする(公式ドキュメントの例)

private lateinit var binding: ActivityMainBinding

@Override
fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

ViewBindingで自動生成されたクラスにinflateメソッドがあるのでそれでViewを生成してActivityに渡しています。
でもlateinitはできれば使いたくないですよね。

以下のようにも書く事ができます。Lazyを使うとFragmentでは不都合が発生しますが、それは後述します。

private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

@Override
fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
}

既にあるViewを使用する

というのも、AppCompatActivityやFragmentのコンストラクタにLayoutXMLを引数にとってViewを生成することができるようになりました。
既にViewがある場合は以下のようにしてViewBindingのクラスを生成することができます。

ActivityMainBinding.bind(view)

Activityで素直にroot viewを取得する方法は無いっぽくて、以下のコードで取得することができました。

findViewById<ViewGroup>(android.R.id.content)[0]

それを踏まえてコードを書くと以下のようになります。

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    private val binding by lazy { ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content)[0]) }

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

楽に書く

いちいち

findViewById<ViewGroup>(android.R.id.content)[0]

と書くのは面倒なのでDelegation(移譲)を使用して楽に書きましょう。

inline fun Activity.bindBinding(
): Lazy<T> = lazy { ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content)[0]) }

これによって以下のように書けるようになりましたが、これではActivityMainBindingでしか使えないコードです。

private val binding by bindBinding()

DataBindingならDataBindingUtilを使用して以下のようにできますが、ViewBindingにはありません。

DataBindingUtil.bind<ActivityMainBinding>(view)

ViewBindingUtilを作る

ViewBindingのbindやinflateはstaticなメソッドで自動生成されるのでリフレクションするしか無さそう?というわけで作ってみました。

class ViewBindingUtil {
    companion object {
        inline fun <reified T : ViewBinding> bind(view: View): T {
            return bind(view, T::class.java)
        }

        fun <T : ViewBinding> bind(view: View, clazz: Class<T>): T {
            return clazz
                .getMethod("bind", View::class.java)
                .invoke(null, view) as T
        }

        inline fun <reified T : ViewBinding> inflate(
            inflater: LayoutInflater
        ): T {
            return inflate(inflater, T::class.java)
        }

        fun <T : ViewBinding> inflate(
            inflater: LayoutInflater,
            clazz: Class<T>
        ): T {
            return clazz
                .getMethod("inflate", LayoutInflater::class.java)
                .invoke(null, inflater) as T
        }

        inline fun <reified T : ViewBinding> inflate(
            inflater: LayoutInflater,
            root: ViewGroup?,
            attachToRoot: Boolean
        ): T {
            return inflate(inflater, root, attachToRoot, T::class.java)
        }

        fun <T : ViewBinding> inflate(
            inflater: LayoutInflater,
            root: ViewGroup?,
            attachToRoot: Boolean,
            clazz: Class<T>
        ): T {
            return clazz
                .getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
                .invoke(null, inflater, root, attachToRoot) as T
        }
    }
}

(そのうちJitPackに上げようかな)

というわけでこのように書けるようになりました。

inline fun <reified T : ViewBinding> Activity.bindBinding(
): Lazy<T> = lazy { ViewBindingUtil.bind<T>(findViewById<ViewGroup>(android.R.id.content)[0]) }
class MainActivity : AppCompatActivity(R.layout.activity_main) {
    private val binding by bindBinding<ActivityMainBinding>()
}

Fragmentで使用する

FragmentにはViewの再生成があるのでby lazyを使ってしまうと画面回転した際に古いViewが参照されてViewが更新されなくなってしまいます。

なので以下のようにする必要があります。毎回処理が走りますが、Viewが10個くらいのXMLなら毎回生成しても速度はほとんど変わらなかったです。(ループで1万回で検証)

class MainFragment : Fragment(R.layout.fragment_main) {
    private val binding get() = FragmentMainBinding.bind(view)
}

昔のDataBindingならコードジャンプで自動生成のコードに飛べたのですが、ViewBindingではXMLに飛んでしまうのでFragmentMainBinding.bindの中でどんな処理をしているのかわかってません。APK覗くの以外でどうやって自動生成のコード見れるんだこれ。

毎回生成するのが気になった場合

viewのhashでキャッシュしてあげれば良いんじゃないですかね。

inline fun <reified T : ViewBinding> Fragment.bindBinding(
): ReadOnlyProperty<Fragment, T> = FragmentViewBindingProvider({ requireView() }, T::class.java)

class FragmentViewBindingProvider<T : ViewBinding>(
    private val viewProvider: () -> View,
    private val clazz: Class<T>
) : ReadOnlyProperty<Fragment, T> {

    private var beforeBinding: T? = null
    private var beforeHashCode: Int? = null

    override operator fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        val view = viewProvider()
        val hashCode = view.hashCode()
        return if (beforeHashCode == null || beforeHashCode != hashCode) {
            beforeHashCode = hashCode

            ViewBindingUtil.bind(view, clazz).also {
                beforeBinding = it
            }
        } else {
            beforeBinding!!
        }
    }
}

その他のViewで使用する。

こんな感じですかね。ViewBindingUtilがあれば色々書けるのでいい感じに書いていきましょう。

inline fun <reified T : ViewBinding> inflateBinding(
    crossinline layoutInflaterProvider: () -> LayoutInflater
) = lazy { ViewBindingUtil.inflate(layoutInflaterProvider.invoke(), T::class.java) }

おわりに

というわけで自分がViewBindingを使用するならこんな感じで使用していくという感じの記事でした。
ViewBindingを今後がっつり使っていこうと思います。
でも名前を変更したときとか生成され直さないなど結局はコード生成なんだなという感じでした。