31
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

食べログAdvent Calendar 2020

Day 14

Kotlin Android Extensions syntheticsのdeprecatedに伴う対応

Last updated at Posted at 2020-12-13

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アプリケーションを作る」です。お楽しみに!!

参考

31
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?