android-kotlin-extensionsからViewBindingへの移行

Kotlin Android ExtensionsのSynthetic viewsは、findViewById()を使わずViewのIDで直接参照できるので、とても便利に利用していたのですが、残念ながらKotlin 1.4.20でDeprecatedになってしまいました。(Kotlin 1.4.20 Released: 真ん中よりちょっと下あたり、Deprecation of Kotlin Android Extensionsに書かれています)

対応方法としては、ViewBindingに移行すべしとのことです。基本的なActivityとFragmentについてはAndroid Developerに移行ガイドが書かれていますが、実際に移行作業をすると、このガイドだけでは不十分なところもありましたので、まとめておきたいと思います。

ちなみにKotlin Android ExtensionsのSynthetic viewsは2021年9月以降のKotlinのリリースでは削除されるようですよ。それまでには対応を完了させておきたいところですね。

それでは移行手順を確認していきましょう。

build.gradleにViewBindingを追加

これは公式の移行ガイドに載っている通りです。build.gradleviewBinding = trueを追加します。

android {
    ...
    buildFeatures {
        viewBinding = true
    }
}

公式ガイドでは同時にandroid-kotlin-extensionsbuild.gradleから削除していますが、Synthetic viewsを使っている人は、アプリの至る所で使っていることと思います。いきなり削除してしまうと、アプリのビルドを通すのが大変になってしまうので、ActivityやFragmentを一つずつViewBindingに切り替えていき、最後にandroid-kotlin-extensionsの削除を行うのがよいと思います。

各ファイルの移行作業

ここからは、android-kotlin-extensionsを利用していたFragmentやActivity一つ一つについて移行作業をしていきます。アプリ全体を一度に作業しようとすると、全部移行作業が終わるまで全く動作確認できなくなってしまいますので、ファイル一つずつ順に動作確認しながら移行作業を進めていくことをお勧めします。

共通の作業

ActivityやFragmentなどの違いに関係なく、まずはkotlinx.android.syntheticのimport文を削除します。syntheticの後ろにはLayout XMLのファイル名が続いているはずです。例えばactivity_sample.xmlというレイアウトがあった場合は、次のようなimport文があることと思いますので、削除します。

import kotlinx.android.synthetic.main.activity_sample.*

Activityの場合

ここからはUIの種類によって対処が異なります。Activityの場合、bindingクラスオブジェクトはlateinit varで保持します。生成されるbindingクラスの名前は、レイアウトXMLファイル名のパスカルケース(単語の先頭が大文字)になっています。例えばレイアウトXMLがactivity_sample.xmlの場合は、bindingクラス名はActivitySampleBindingになります。

private lateinit var binding: ActivitySampleBinding

次にonCreateでbindingをinflateし、bindingから取得したルートビューをActivityのsetContentViewに渡します。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivitySampleBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

あとは、bindingオブジェクト経由で、レイアウトXMLに定義したViewのIDを使って、各Viewを参照することができます。レイアウトにtitleTextViewというidTextViewがあったとすると、

<TextView
    android:id="@+id/titleTextView"
    ...
/>

Activity側では

binding.titleTextView.text = "Title"

のように参照することができます。android-kotlin-extensionsでは直接titleTextViewで該当するViewにアクセスできていたので、前にbinding.が追加される形ですね。若干コードが冗長に感じますが、仕方ないです。

Fragmentの場合

bindingオブジェクトの保持方法は、Activityと少し異なります。bindingオブジェクトを保持するnullableな変数を用意し、実際にViewにアクセスするときはNotNullなgetterを通じてbindingオブジェクトを参照します。

private var _binding: FragmentSampleBinding? = null
private val binding get() = _binding!!

わざわざこのような形をとっている理由は、ViewがDestroyされるときにbindingオブジェクトを削除する必要があるからです。bindingオブジェクトは、onCreateViewからonDestroyViewまでの間で参照できることになります。

bindingのgetterには、Not-Null assertion operator (ビックリマーク2個)が使われています。なかなか推奨されないこの演算子ですが、今回の場合、onCreateViewからonDestroyViewの間の期間であれば確実にbindingオブジェクトが存在し、それ以外の期間はそもそもViewにアクセスするコードが動作しないので、Not-Nullとして扱うことには妥当性があります。実際にbindingオブジェクトを使う際には、binding.titleTextView.textのように後ろにいくつものプロパティやメソッドがつながることになるので、bindingオブジェクトがNot-Nullのほうが記述がすっきりします。

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    _binding = FragmentSampleBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

onDestroyView_bindingにnullを代入し、bindingクラスのオブジェクトへの参照を削除しています。また、Activityの場合と違い、inflateの引数にViewGroupを指定していることも注意します。

DialogFragmentの場合

DialogFragmentもFragmentのサブクラスということで、Fragmentの場合と処理は近いです。nullableな変数でbindingオブジェクトを保持し、onDestroyViewで開放するのはFragmentの場合と同じです。

class SampleDialog: DialogFragment() {
    private var _binding: SampleDialogBinding? = null
    private val binding get() = _binding!!

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        _binding = SampleDialogBinding.inflate(requireActivity().layoutInflater)
        binding.titleTextView.text = "Title"
        return AlertDialog.Builder(requireActivity())
            .setView(binding.root)
            .create()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

inflateに渡すinflaterは、requireActivity()経由で取得します。そして、BuilderのsetViewbinding.rootを渡します。

RecyclerView.ViewHolderの場合

RecyclerViewの要素を表示するためのViewでSynthetic viewsを使っていた場合も、ViewBindingに置き換えることができます。bindingオブジェクトはAdapteronCreateViewHolderでinflateし、ViewHolderに渡して、ViewHolderが保持します。そして、onBindViewHolderでbindingオブジェクト経由で各Viewにアクセスし、表示内容を更新します。

class SampleAdapter(): RecyclerView.Adapter<SampleViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = SampleViewBinding.inflate(layoutInflater, parent, false)
        return SampleViewHolder(binding)
    }

    override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
        holder.binding.titleTextView.text = "Title"
    }
}

class SampleViewHolder(val binding: SampleViewBinding): RecyclerView.ViewHolder(binding.root)

Fragmentの場合と同じく、inflateの引数にViewGroupを指定しています。

android-kotlin-extensionsを削除

ここまでで、Activity, Fragment, DialogFragment, RecyclerViewについては一通りの移行が完了していることと思います。他の種類のViewがもしあれば、同じようにViewBindingに移行し、最後にandroid-kotlin-extensionsbuild.gradleから削除します。一通りのViewの移行ができていれば、android-kotlin-extensionsを削除してもエラーにはならないはずです。

apply plugin: 'kotlin-android-extensions'

まとめ

お疲れ様でした。android-kotlin-extensionsからViewBindingへの移植を見ていきました。ActivityやFragmentの数が多いとなかなか大変ですが、いったんやり方を理解すればほとんど単純作業なので、それほど難しくはないと思います。完全に使えなくなってしまう前に、移行してしまいましょう。