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.gradle
にviewBinding = true
を追加します。
android {
...
buildFeatures {
viewBinding = true
}
}
公式ガイドでは同時にandroid-kotlin-extensions
もbuild.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
というid
のTextView
があったとすると、
<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のsetView
にbinding.root
を渡します。
RecyclerView.ViewHolderの場合
RecyclerViewの要素を表示するためのViewでSynthetic viewsを使っていた場合も、ViewBindingに置き換えることができます。bindingオブジェクトはAdapter
のonCreateViewHolder
で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-extensions
をbuild.gradle
から削除します。一通りのViewの移行ができていれば、android-kotlin-extensions
を削除してもエラーにはならないはずです。
apply plugin: 'kotlin-android-extensions'
まとめ
お疲れ様でした。android-kotlin-extensions
からViewBindingへの移植を見ていきました。ActivityやFragmentの数が多いとなかなか大変ですが、いったんやり方を理解すればほとんど単純作業なので、それほど難しくはないと思います。完全に使えなくなってしまう前に、移行してしまいましょう。