Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?

Kotlin Android Extensions syntheticsのdeprecatedに伴う対応

11/23にAndroid Developers Blogで公開されたKotlin Android Extensions の未来 [1]にてsyntheticsがKotlin 1.4.20よりdeprecatedになることが示されました。
「食べログテイクアウト」のAndroidアプリでもsyntheticsは使用しているので、deprecatedになった経緯や必要な対応などをまとめようと思います。

syntheticsとは

↓こんな感じでレイアウトのidに対応したimport文を書くだけで

import kotlinx.android.synthetic.main.fragment_hogehoge_layout.fugaButton

Viewにアクセスできる機能のことです。

fugaButton // Viewにアクセス
  .setOnSingleClickListener {
    // do something
  }

syntheticsはKotlin Android Extensionsプラグインに含まれていて、JetBrainsが開発しました。
非常に便利なこの機能でしたが、GoogleとしてはJetBrainsと連携してメンテナンスを続けるコストが高いと判断し、deprecatedに至ったようです。

私たちは、JetBrains とともに、synthetics のメンテナンスを続けることの賛否について議論してきました。
引用:Kotlin Android Extensions の未来 [1]

廃止理由

deprecatedに至ったのには、以下のような理由があります。
以下に主なものを並べてみますが
公式[1]やこちら[3]のStackOverflowのページなどがわかりやすかったです。

▶︎関係のないクラスから間違ったViewを呼び出すと実行時例外になる

例えばフラグメントAとBとそれぞれのフラグメントに対応したレイアウトA'とB'があったとします。
この時フラグメントBでレイアウトA'のViewをimportしてアクセスすることも可能ですが、inflateしてないためnullにしかならず、実行時に例外を引き起こします。

class HogeFragment {
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    // HogeFragment用ではないView fugaTextView
    fugaTextView.text = "ほげほげ"
  }
}

ビルドは通るが実行時に落ちる

2020-12-06 16:20:41.934 31810-31810/com.hoge_app.xxx.debug E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.hoge_app.xxx.debug , PID: 31810
    java.lang.IllegalStateException: fugaTextView must not be null
        at com.hoge_app.xxx.yyy.zzz.HogeFragment.onViewCreated(HogeFragment.kt:XX)

▶︎Kotlinでしか使えない

Kotlin Android Extensionsの機能なのでJavaを採用しているプロジェクトでは使えません。

▶︎View Bindingで代替できる

今後はView Bindingを使用する必要があります。
View BindingはJavaでも使用できます。
View Binding(ビュー バインディング)

View Bindingにデメリットはある?

若干コード記述量が増えるくらいで、特に大きなデメリットは無いようです。

// View Binding
binding.hogeButton.setOnClickListener { 
  // do something
}

// synthetics
hogeButton.setOnClickListener { 
  // do something
}

必要な対応

View Bindingへの置き換え(導入方法)

1. build.gradleに追記する

View Bindingはモジュール単位で有効化します。

android {
        ...
        viewBinding {
            enabled = true
        }
    }

これにより、レイアウトのXMLファイルごとにファイル名に対応したクラスが自動で生成されるようになります。
クラス名はファイル名をlower camel case化+SuffixにBindingとなっています。

activity_main.xml → ActivityMainBinding

モジュール内でビュー バインディングを有効にすると、そのモジュール内に存在する XML レイアウト ファイルごとにバインディング クラスが生成されます。
Android Developer: ビュー バインディング

バインディングクラスを生成したく無いファイルは以下のようにルートビューにtools:viewBindingIgnore="true"を指定します。

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

このBindingクラスはレイアウトのルートビューへの参照とIDを持つ全てのViewへの参照を持っています。

なお、build.gradleファイルに追記後以下のようなエラーが出る場合は、Android Studioのバージョンが古いので3.6以降にアップデートする必要があります。

Could not find method viewBinding() for arguments [build_ay2mrmfdhm1ff92kdd2n9jncc$_run_closure1$_closure9@3ae02128] on object of type com.android.build.gradle.internal.dsl.BaseAppModuleExtension.
Open File

注: ビュー バインディングを利用できるのは、Android Studio 3.6 Canary 11 以降に限られます。
Android Developer: View Binding(ビュー バインディング)

2. アクティビティで使用する

onCreatesetContentViewにバインディングクラスからinflateしたViewをセットします。


    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

これだけで、Viewの要素にアクセスできるようになります。

binding.hogeTextView.text = "hogehoge"
binding.fugaButton.setOnClickListener { // do something }

3. フラグメントで使用する

フラグメントの場合はonCreateView()でバインディングクラスからinflateしたViewを返します。


    private var _binding: FragmentHogeBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

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

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

公式の注意文にもあるように、フラグメントの場合はメモリリークしないようにバインディングクラスで確保したメモリ領域をonDestroyViewのタイミングで解放してあげる必要があります。
_binding = null

注: フラグメントはビューよりも持続します。フラグメントの onDestroyView() メソッドでバインディング クラスのインスタンスへの参照をすべてクリーンアップしてください。
Android Developer: フラグメントでビュー バインディングを使用する

最後に

最初はsyntheticsからViewBindingへの移行って結構面倒臭いのかな?と思っていたのですが、簡単に移行できそうなことが分かり良かったです!
特に調査でめちゃくちゃハマったところは無く、公式の情報があればdeprecatedの経緯の把握やViewBindingの導入は問題無いと思いました。
syntheticsは簡単で非常に便利なので個人的にとても好きでしたが、nullになるViewが関係の無いフラグメントで参照できたりといった点は確かにイマイチだな〜と思っていたので食べログテイクアウトでも早くViewBindingに移行したいです。
ここまで見ていただきありがとうございました!

明日は、@tsun3 さんの「RustだけでWebアプリケーションを作る」です。お楽しみに!!

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What are the problem?