機能の詳細については公式ドキュメントを見てもらうとして、この機能で data binding の時のように生成される Binding クラスは ViewBindingという interface を実装している。
/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package androidx.viewbinding;
import android.view.View;
import androidx.annotation.NonNull;
/** A type which binds the views in a layout XML to fields. */publicinterface ViewBinding {
/** * Returns the outermost {@link View} in the associated layout file. If this binding is for a * {@code <merge>} layout, this will return the first view inside of the merge tag. */@NonNull
View getRoot();
}
Version Code mean nothing for Libraries. It's actually potentially confusing as one might expect that the BuildConfig.VERSION_NAME of a library is set to the version name of the app but this was never the case. For these reasons, it's better to not include it in library modules.
java.lang.RuntimeException: android.content.pm.PackageParser$PackageParserException: <path to apk for local test>: Requires newer sdk version #29 (current version is #28)
解決方法
動かせた頃の image を使う
CircleCI Japan のアカウントで紹介されているように*7、JDK 8 ベースだった時の docker image の digest を、使用する image の指定に追記することで使う image の固定ができる。今回の件で知ったが、これは document でも best practice として言及されている*8。
今のところ簡単なことしかやってなくて、とりあえず Builder class でのオブジェクト生成を scope function で書けるようにしたのと、play-services-cast-framework向けに RemoteMediaClientなどにある PendingResultを用いた通信を suspending function として呼べるようにした程度。 play-services-cast-tvの Modifier や Writer 向けにも scope function 使えるようにしようかと考えてるけどこの辺まだ殆ど触ってないので使用感がわからず手をつけてない。
Builder をネストできる箇所があって、そういう所は scope function で表現すると呼べる関数が混ざって事故るよなーとは思ってて、 @DslMarker使ってアクセスできる receiver の制限できないか試してみたけど、筋よく実装できなそうなので諦めた。
外部のライブラリに対して @DslMarkerの機能を有効にするには、対象を継承した自作 class を作ってそれに @DslMarkerを適用した annotation class を当て、そちらの class を receiver にした lambda を受け取る関数を実装する必要がある。しかし Cast SDKの Builder class の中には LaunchOptions.Builderなど final class になってて継承できないものがあり、それらは wrapper class みたいなものを書かないと receiver の制限を実現できない。そんな class はメンテ面倒だし、 Builder class が継承可能というのも変な話で今後他の Builder もfinalを付けられていくのが妥当だろうと思う。
DefaultTaskExecutorの postToMainThreadでは、main looper に対する Handlerを使ってタスクを main thread に post している。 isMainThreadは、 main looper に紐ついてる thread と current thread を比較している。
そもそもなんで使う必要があるのかというと、Androidアプリとしてコードを実行する時と local test を実行する時の環境の差が関係している。Androidアプリとして実行される際は、大雑把に説明すると、アプリのプロセスが起動した後まず ActivityThreadで main looper の準備と実行が行われ*1、それから諸々の準備ができた後に我々の書いたアプリケーションコードが動く。しかし local test は JUnit test として実行されるので、Androidアプリに関わるセットアップの類は実行されない。これがどう関係するかというと、local test 実行時には main looper のセットアップが行われず Looper#getMainLooperは nullのままとなるので、それに依存する Handlerや、 main looper をもとに "Androidアプリにおける main thread"を判定する処理が機能しなくなってしまう。
class MyTest {
fun test() {
val mainLooper = Looper.getMainLooper() // nullval livedata = MutableLiveData<Int>()
livedata.observeForever { println("onChanged") }
// java.lang.NullPointerException// at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread// at androidx.arch.core.executor.ArchTaskExecutor.isMainThread// at androidx.lifecycle.LiveData.assertMainThread// at androidx.lifecycle.LiveData.observeForever
}
}
この問題に対しては 1) main looper をテスト実行前に用意する、もしくはあるように偽装する、2) Looperや Handlerに触れない形に変える、という解決方法を考えることができ、 2) を実現する手段として InstantTaskExecutorRuleが用意されている、ということになる。
Robolectric による解決と比較
ところで Robolectricを使っても main looper の問題を解決することは可能である。Robolectric はテスト実行時に Android framework のコードを実行したり APIを偽装してくれたりする。つまり上で挙げた 1) のアプローチを取ることになる。今回のケースでは、テストケース実行時には Looperに main looper が準備された状態になっているため、 ArchTaskExecutorで DefaultTaskExecutorを使っても NPE でクラッシュすることはなくなる。
@RunWith(AndroidJUnit4::class)
class MyTestWithRobolectric {
fun `test with robolectric`() {
val mainLooper = Looper.getMainLooper() // non-null valueval livedata = MutableLiveData<Int>()
livedata.observeForever { println("onChanged") } // observe できる
}
}
class MyTestWithInstantTaskExecutorRule {
@get:Ruleval instantTaskExecutorRule = InstantTaskExecutorRule()
fun `test with InstantTaskExecutorRule`() {
val livedata = MutableLiveData<Int>()
var count = 0
livedata.observeForever {
println("onChanged: $it")
count++
}
livedata.value = 1// ArchTaskExecutor#postToMainThead の振る舞いが同期化されるので、// postValue もテスト実行時に observer に通知が届くようになる
livedata.postValue(2)
assertThat(count).isEquelTo(2) // passed
}
}
使い分けとしては, LiveData や Room しか関わらないような local test については InstantTaskExecutorRuleだけ使って、他にも Android framework への依存があるようなコンポーネントに対する local test では Robolectric も使う、みたいな感じで良いと思う。
publicstaticvoid injectIfNeededIn(Activity activity) {
if (Build.VERSION.SDK_INT >= 29) {
// On API 29+, we can register for the correct Lifecycle callbacks directly
LifecycleCallbacks.registerIn(activity);
}
// Prior to API 29 and to maintain compatibility with older versions of// ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and// need to support activities that don't extend from FragmentActivity from support lib),// use a framework fragment to get the correct timing of Lifecycle events
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
// Hopefully, we are the first to make a transaction.
manager.executePendingTransactions();
}
}
// ...@RequiresApi(29)staticclass LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
// ...
}
ここで NonConfig と呼ばれる FragmentManagerViewModelは、端的に言うと Activity の ViewModelStoreに登録される ViewModel で、それぞれの Fragment に対応する ViewModelStoreの管理や Activity の再生成時に retain される Fragment の保持といった役割を持っている。 clearNonConfigState()では、与えられた Fragment に対応する ViewModelStoreと子の NonConfig の clear および管理下からの削除を行っている。
void clearNonConfigState(@NonNull Fragment f) {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "Clearing non-config state for " + f);
}
// Clear and remove the Fragment's child non config state
FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho);
if (childNonConfig != null) {
childNonConfig.onCleared();
mChildNonConfigs.remove(f.mWho);
}
// Clear and remove the Fragment's ViewModelStore
ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
if (viewModelStore != null) {
viewModelStore.clear();
mViewModelStores.remove(f.mWho);
}
}
Through discussions with adamp@, we wanted to make a firm decision on exactly when this will run as the final LifecycleObserver going out. onDestroy() does not offer the same guarantee since developers can call it at any point in their onDestroy() method.
ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
at androidx.lifecycle.ReportFragment$LifecycleCallbacks.onActivityPreDestroyed(ReportFragment.java:224)
at android.app.Activity.dispatchActivityPreDestroyed(Activity.java:1498)
at android.app.Activity.performDestroy(Activity.java:8241)
at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:144)
at androidx.lifecycle.ReportFragment.onDestroy(ReportFragment.java:134)
at android.app.Fragment.performDestroy(Fragment.java:2782)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1451)
at android.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1576)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1637)
at android.app.FragmentManagerImpl.dispatchMoveToState(FragmentManager.java:3046)
at android.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:3026)
at android.app.FragmentController.dispatchDestroy(FragmentController.java:248)
at android.app.Activity.performDestroy(Activity.java:7394)
at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1306)
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4443)
at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4476)
at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:39)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
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)
Fragment
ViewModel cleared at com.github.nashcft.app.FragmentViewModel.onCleared(FragmentViewModel.kt:23)
at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
at androidx.fragment.app.FragmentManagerViewModel.clearNonConfigState(FragmentManagerViewModel.java:199)
at androidx.fragment.app.FragmentStateManager.destroy(FragmentStateManager.java:769)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:350)
at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.java:742)
at androidx.fragment.app.SpecialEffectsController$Operation.cancel(SpecialEffectsController.java:594)
at androidx.fragment.app.SpecialEffectsController.forceCompleteAllOperations(SpecialEffectsController.java:329)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3116)
at androidx.fragment.app.FragmentManager.dispatchDestroy(FragmentManager.java:3091)
at androidx.fragment.app.FragmentController.dispatchDestroy(FragmentController.java:334)
at androidx.fragment.app.FragmentActivity.onDestroy(FragmentActivity.java:322)
at com.github.nashcft.app.MyFragmentActivity.onDestroy(MyFragmentActivity.kt:43)
at android.app.Activity.performDestroy(Activity.java:8245)
at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
*1:余談だが androidx.core にも ComponentActivity が存在し、 androidx.activity.ComponentActivity の直接の親 class となっている
> Configure project :
Gradle version Gradle 7.0
FAILURE: Build failed with an exception.
* Where:
Script '/script/maven-plugin.gradle' line: 2
* What went wrong:
A problem occurred evaluating script.
> Failed to apply plugin 'com.github.dcendents.android-maven'.
> Could not create plugin of type 'AndroidMavenPlugin'.
> Could not generate a decorated class for type AndroidMavenPlugin.
> org/gradle/api/publication/maven/internal/MavenPomMetaInfoProvider
...
Getting tasks: ./gradlew tasks --all
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -Dhttps.protocols=TLSv1.2
Tasks:
WARNING:
Gradle 'install' task not found. Please add the 'maven' or 'android-maven' plugin.
See the documentation and examples: https://jitpack.io/docs/
Adding android plugin
Adding maven plugin
Found android library build file in cast-ktx
Found android library build file in cast-framework-ktx
Found android library build file in cast-tv-ktx
Running: ./gradlew clean -Pgroup=com.github.nashcft -Pversion=0.1.0 install
...
When using Android Gradle plugin 7.0 to build your app, JDK 11 is now required to run Gradle. Android Studio Arctic Fox bundles JDK 11 and configures Gradle to use it by default, which means that mostAndroid Studio users do not need to make any configuration changes to their projects.
Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR. Previous versions of the Android Gradle Plugin produce broken AARs in this case too (despite not throwing this error). The following direct local .aar file dependencies of the :[module name] project caused this error: [path to .aar file]