環境
- OpenJDK 11
- Gradle 7.1.1
- Android Gradle Plugin 4.2 ~ (7.0.0, 7.1.0-alpha08 でも再現確認)
- Gradle Multimodule 構成
発生条件とエラー内容
Android Studio や CI でビルドしたアプリが、特定の画面へ遷移したときに以下のエラー(クラッシュ)が発生することがある。
- AppCompatActivity(contentLayoutId: Int) または Fragment(contentLayoutId: Int) を使ってレイアウトを自動生成したあとに DataBindingUtil.bind(root: View) で DataBinding を取得する実装をしている
- モジュール数が多いと発生しやすい
-
./gradlew clean :app:kaptReleaseKotlin --no-build-cache
のように DataBinding のキャッシュが存在しない状態だと再現しやすい
Caused by: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/activity_main_0
2021-08-13 18:44:50.658 21905-21905/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.app, PID: 21905
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.app/com.example.app.MainActivity}: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/activity_main_0
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2956)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3091)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:280)
at android.app.ActivityThread.main(ActivityThread.java:6744)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/activity_main_0
at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:185)
at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:152)
at com.wada811.databinding.ActivityDataBinding$dataBinding$1.bind(ActivityDataBinding.kt:26)
at com.wada811.databinding.ActivityDataBinding$dataBinding$1.getValue(ActivityDataBinding.kt:15)
at com.wada811.databinding.ActivityDataBinding$dataBinding$1.getValue(ActivityDataBinding.kt:11)
at com.example.app.MainActivity.getBinding(MainActivity.kt:44)
at com.example.app.MainActivity.onCreate(MainActivity.kt:170)
at android.app.Activity.performCreate(Activity.java:7136)
at android.app.Activity.performCreate(Activity.java:7127)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2936)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3091)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:280)
at android.app.ActivityThread.main(ActivityThread.java:6744)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
原因
DataBinding は kapt{variant}Kotlin
タスクで {module}/build/generated/source/kapt/{variant}/{package}.../DataBinderMapperImpl.java
を生成します。
DataBinderMapperImpl には以下のような collectDependencies() が生成されており、そのモジュールが依存しているサブモジュールの DataBinderMapperImpl を取得できるようになっています。
@Override
public List<DataBinderMapper> collectDependencies() {
ArrayList<DataBinderMapper> result = new ArrayList<DataBinderMapper>(10);
result.add(new androidx.databinding.library.baseAdapters.DataBinderMapperImpl());
result.add(new com.example.app.feature.feature1.DataBinderMapperImpl());
result.add(new com.example.app.feature.feature2.DataBinderMapperImpl());
...
return result;
}
ところが、DataBinding の kapt{variant}Kotlin
タスクは依存しているサブモジュールの :{module}:kapt{variant}Kotlin
タスクの完了を待っておらず、サブモジュールの DataBinderMapperImpl を認識できないまま DataBinderMapperImpl.collectDependencies() を生成してしまうことがあるようです。
不完全な DataBinderMapperImpl であってもアプリのビルドは成功します。該当の画面で DataBindingUtil.bind() を実行すると、取りこぼされたモジュールの DataBinderMapperImpl を見つけることができずにエラーとなります。
すべてのサブモジュールの DataBinderMapperImpl が生成されたあとであれば、親モジュールの DataBinderMapperImpl は正しく生成されるため、ローカルの開発環境などで何度もビルドしている環境ではこの問題は発生しづらいようです。
対策
この問題が修正されるまでは以下のワークアラウンドが有効です。
- CI 環境で、リリースビルド前に
./gradlew kapt{variant}Kotlin
を実行しておく
例:
# すべてのサブモジュールの kapt{variant}Kotlin を実行
# モジュールごとに {variant} が異なる場合は Gradle スクリプトなどで工夫しても良いかも
./gradlew kaptReleaseKotlin
./gradlew bundleRelease
各モジュールに生成された DataBinderMapperImpl.java を眺めて、collectDependencies() が漏れなく実装されていることが確認できれば解決です。
その他
- 厳密にはモジュールの依存関係を逆順に辿って一つずつ kapt タスクを実行していく必要があるかもしれませんが、現実にはそこまで階層の深いマルチモジュールは想定しづらいため、すべてのサブモジュールの kapt タスクを1回実行できればよしとしています
-
./gradlew kaptReleaseKotlin bundleRelease
のように gradlew コマンドをまとめるとうまくいきませんでした -
./gradlew bundleRelease --no-parallel
のように並列実行を無効としても問題は解消されませんでした - gradle.properties
kapt.use.worker.api=false
を設定したところ、Android Studio からのビルドでは問題が解消しているように見えましたが、CI (Github Actions) では引き続き問題が発生しているようでした - DataBinding kapt プラグイン側で修正して欲しいところですが、DataBinding と kapt キャッシュは今までもトラブルが多かったので、今後も DataBinding kapt プラグインに期待することは難しいかもしれません。DataBinding が ksp に置き換わればあるいは?
- Jetpack Compose が普及すれば DataBinding kapt に頼る必要はなくなる、と期待するのがベストかもしれません