20210415のAndroidに関する記事は5件です。

DaggerHiltのAndroidEntryPointのCustomViewのInjectのタイミングメモ

以下のような場合に問題ないのかが気になっていました。 これ、クラッシュしそうですよね?? @Inject lateinit var dep: Dep val value = dep.value class Dep @Inject constructor() { val value = "aaaa" } @AndroidEntryPoint class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { @Inject lateinit var dep: Dep val value = dep.value } これは以下のようなコードを生成し、CustomViewが継承するようになります。つまりコンストラクタ内でinjectされます。 public abstract class Hilt_CustomView extends FrameLayout implements GeneratedComponentManagerHolder { private ViewComponentManager componentManager; private boolean injected; Hilt_CustomView(Context context) { super(context); inject(); } Hilt_CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); inject(); } Hilt_CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); inject(); } @TargetApi(21) Hilt_CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); inject(); } ... CustomViewの変換後のdecompile後は以下のようになります。 以下のようにsuperのコンストラクタが呼ばれたあと、つまりinjectされた後にgetValue()などが行われるため、問題なさそうです 抜粋 super(context, attrs, defStyleAttr); Dep var10001 = this.dep; if (var10001 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } this.value = var10001.getValue(); public final class CustomView extends Hilt_CustomView { @Inject public Dep dep; @NotNull private final String value; @NotNull public final Dep getDep() { Dep var10000 = this.dep; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } return var10000; } public final void setDep(@NotNull Dep var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.dep = var1; } @NotNull public final String getValue() { return this.value; } @JvmOverloads public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { Intrinsics.checkNotNullParameter(context, "context"); super(context, attrs, defStyleAttr); Dep var10001 = this.dep; if (var10001 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } this.value = var10001.getValue(); } // $FF: synthetic method public CustomView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) { if ((var4 & 2) != 0) { var2 = (AttributeSet)null; } if ((var4 & 4) != 0) { var3 = 0; } this(var1, var2, var3); } @JvmOverloads public CustomView(@NotNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0, 4, (DefaultConstructorMarker)null); } @JvmOverloads public CustomView(@NotNull Context context) { this(context, (AttributeSet)null, 0, 6, (DefaultConstructorMarker)null); } 色々いじってみる 複数パターン 以下のような場合も一応見てみましょう @AndroidEntryPoint class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { @Inject lateinit var dep: Dep val value = dep.value @Inject lateinit var dep2: Dep val value2 = dep2.value } 問題なさそうでした。 public final class CustomView extends Hilt_CustomView { @Inject public Dep dep; @NotNull private final String value; @Inject public Dep dep2; @NotNull private final String value2; @NotNull public final Dep getDep() { Dep var10000 = this.dep; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } return var10000; } public final void setDep(@NotNull Dep var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.dep = var1; } @NotNull public final String getValue() { return this.value; } @NotNull public final Dep getDep2() { Dep var10000 = this.dep2; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep2"); } return var10000; } public final void setDep2(@NotNull Dep var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.dep2 = var1; } @NotNull public final String getValue2() { return this.value2; } @JvmOverloads public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { Intrinsics.checkNotNullParameter(context, "context"); super(context, attrs, defStyleAttr); Dep var10001 = this.dep; if (var10001 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } this.value = var10001.getValue(); var10001 = this.dep2; if (var10001 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep2"); } this.value2 = var10001.getValue(); } コンストラクタを後ろに書くパターン @AndroidEntryPoint class CustomView : FrameLayout { @Inject lateinit var dep: Dep val value = dep.value @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : super(context, attrs, defStyleAttr) } 問題なさそう public final class CustomView extends Hilt_CustomView { @Inject public Dep dep; @NotNull private final String value; @NotNull public final Dep getDep() { Dep var10000 = this.dep; if (var10000 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } return var10000; } public final void setDep(@NotNull Dep var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.dep = var1; } @NotNull public final String getValue() { return this.value; } @JvmOverloads public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { Intrinsics.checkNotNullParameter(context, "context"); super(context, attrs, defStyleAttr); Dep var10001 = this.dep; if (var10001 == null) { Intrinsics.throwUninitializedPropertyAccessException("dep"); } this.value = var10001.getValue(); } superに渡すパターン IDE上、コンパイラでエラーになるので問題なさそうでした。 @AndroidEntryPoint class CustomView : FrameLayout { @Inject lateinit var dep: Dep val value = dep.value @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : super(context, attrs, defStyleAttr + value.hashCode()) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin 初心者】Preferencesの使い方

Preferences API通信したアプリのtokenなどのデータをローカルに保存させます 参考URL https://developer.android.com/guide/topics/ui/settings/use-saved-values?hl=ja Preferencesに書き込む val shardPreferences = this.getPreferences(Context.MODE_PRIVATE) val shardPrefEditor = shardPreferences.edit() shardPrefEditor.putString("token", token) shardPrefEditor.apply() Preferencesで読み取る val shardPreferences = this.getPreferences(Context.MODE_PRIVATE) var data = shardPreferences.getString("token", "[]")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Gradle plugin requires Java 11と言われてSyncできないときの対処法

AGP 7.0.0-alpha02からJava 11が必須になったようです。 https://developers-jp.googleblog.com/2020/12/announcing-android-gradle-plugin.html あの設定どこだっけってときどきなって、かなり高確率で最初に当たるので、メモしておきます。 An exception occurred applying plugin request [id: 'com.android.application'] > Failed to apply plugin 'com.android.internal.application'. > Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8. You can try some of the following options: - changing the IDE settings. - changing the JAVA_HOME environment variable. - changing `org.gradle.java.home` in `gradle.properties`. Gradleの設定を開いて、 Android Studioに入っているJDKを選ぶだけです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fragmentのwidth, heightの簡単な取得方法

はじめに FragmentのonActivityCreated等でビューの横幅、高さを取得しようとすると0が返ってくると思います。 これはビューの大きさが確定する前に取得しようとしているのが原因なのですが、 この解決策として、今までOnGlobalLayoutListener#onGlobalLayout()を使用していました。 ただこの方法ですと、コード量がちょっと多いです。 postを使ってより短く書ける方法がありましたので、そちらを共有します。 環境 AndroidStudio 4.1 kotlin 1.3.72 コード import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import androidx.fragment.app.Fragment import com.example.kotlinsample.R class SampleFragment : Fragment() { companion object { private const val TAG = "SampleFragment" } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_sample, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // 0. 対策なし Log.d(TAG, "[0] width${view?.width}, height:${view?.height}") // 1. OnGlobalLayoutListenerを使う方法 var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { Log.d(TAG, "[1] width${view?.width}, height:${view?.height}") // リスナ解除 view?.viewTreeObserver?.removeOnGlobalLayoutListener(globalLayoutListener) } view?.viewTreeObserver?.addOnGlobalLayoutListener(globalLayoutListener) // 2. postを使う方法 view?.post { Log.d(TAG, "[2] width${view?.width}, height:${view?.height}") } } } 結果 [0] width0, height:0 [1] width1080, height:1776 [2] width1080, height:1776 対策なしで取得した場合が0になり OnGlobalLayoutListenerを使う方法、postを使う方法で横幅、高さが取得できました。 常に正しく取得できるかは未検証ですが、コード量がシンプルになるので積極的に使っていきたいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(16)ライブラリバージョンアップ2021春(2)

前回のつづきで、今回はAndroidX(Jetpack)のバージョンを上げていきます。 いつもテストの修正までセットにしていましたが、ちょっと長くなりすぎるのでテストの対応は次回に回します。 環境 ツールなど バージョンなど MacbookPro macOS Catalina 10.15.7 Android Studio 4.1.2 Java(JDK) openjdk version "11.0.10" 修正前のbuild.gradle 前回の記事後のappモジュールのbuild.gradleの状態を載せておきます。 前回までのコード app/build.gradle apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-parcelize' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' def keystorePropertiesFile = rootProject.file("keystore.properties") android { compileSdkVersion 29 defaultConfig { applicationId "jp.les.kasa.sample.mykotlinapp" minSdkVersion 19 targetSdkVersion 29 multiDexEnabled true versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' testApplicationId "jp.les.kasa.sample.mykotlinapp.test" resConfigs "ja" } signingConfigs { debug { storeFile file('debug.jks') storePassword 'android' keyAlias = 'androiddebugkey' keyPassword 'android' } release { if (keystorePropertiesFile.exists()) { def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } } buildTypes { debug { resValue "string", "app_name", "(d)歩数計記録アプリ" applicationIdSuffix ".debug" minifyEnabled false signingConfig signingConfigs.debug } release { resValue "string", "app_name", "歩数計記録アプリ" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' if (keystorePropertiesFile.exists()) { signingConfig signingConfigs.release } } } testOptions { unitTests { includeAndroidResources = true returnDefaultValues = true } execution 'ANDROIDX_TEST_ORCHESTRATOR' } buildFeatures { dataBinding true viewBinding true } compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } packagingOptions { exclude 'META-INF/atomicfu.kotlin_module' } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.3.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.2.0-alpha04' implementation 'androidx.fragment:fragment:1.2.1' testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' testImplementation 'androidx.test:runner:1.2.0' testImplementation 'androidx.test:rules:1.2.0' testImplementation 'androidx.test.espresso:espresso-core:3.2.0' testImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' testImplementation 'androidx.test.espresso:espresso-intents:3.2.0' testImplementation 'org.robolectric:robolectric:4.3.1' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'org.assertj:assertj-core:3.2.0' androidTestUtil 'androidx.test:orchestrator:1.2.0' // multidex implementation 'androidx.multidex:multidex:2.0.1' def lifecycle_version = "2.3.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" def arch_version = "2.1.0" // バージョン定義を別出し androidTestImplementation("androidx.arch.core:core-testing:$arch_version") { exclude group: 'org.mockito:mockito-core' } testImplementation "androidx.arch.core:core-testing:$arch_version" // RecyclerView implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01' // ViewPager2 implementation 'androidx.viewpager2:viewpager2:1.0.0' // Room components def room_version = "2.2.6" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" androidTestImplementation("androidx.room:room-testing:$room_version") { exclude group: 'com.google.code.gson' } testImplementation "androidx.room:room-testing:$room_version" // Coroutines api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" // perission dispather implementation "org.permissionsdispatcher:permissionsdispatcher:4.6.0" kapt "org.permissionsdispatcher:permissionsdispatcher-processor:4.6.0" // Koin for Kotlin apps def koin_version = "2.2.2" // Testing testImplementation "org.koin:koin-test:$koin_version" androidTestImplementation "org.koin:koin-test:$koin_version" // Koin for Android implementation "org.koin:koin-android:$koin_version" // Koin AndroidX Scope feature implementation "org.koin:koin-androidx-scope:$koin_version" // Koin AndroidX ViewModel feature implementation "org.koin:koin-androidx-viewmodel:$koin_version" // Koin AndroidX Fragment Factory (unstable version) // implementation "org.koin:koin-androidx-fragment:$koin_version" // FragmentTest testImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:core:1.2.0' // Robolectric用,test向けではなくdebug向けに必要 debugImplementation "androidx.fragment:fragment-testing:1.2.1" debugImplementation 'androidx.test:core:1.2.0' // 無くてもアプリの実行は問題ないがテストがビルド出来ない debugImplementation "androidx.legacy:legacy-support-core-ui:1.0.0" debugImplementation "androidx.legacy:legacy-support-core-utils:1.0.0" // Firebase implementation 'com.google.firebase:firebase-analytics:18.0.2' implementation 'com.google.firebase:firebase-crashlytics:17.3.1' implementation 'com.firebaseui:firebase-ui-auth:6.2.1' implementation 'com.google.firebase:firebase-firestore:22.1.1' // Required only if Facebook login support is required // Find the latest Facebook SDK releases here: https://goo.gl/Ce5L94 implementation 'com.facebook.android:facebook-android-sdk:7.0.0' // Hashids implementation 'org.hashids:hashids:1.0.3' // Mockito testImplementation 'org.mockito:mockito-core:3.7.7' testImplementation 'org.mockito:mockito-inline:3.7.7' } dependenciesが長くなりすぎてるのでいつかどうにかしたいですね。 Android公式/Jetpackを更新する Androidの公式パッケージ、Jetpackを更新していきます。ここは一気に上げないとライブラリ同士がコンフリクト起こすのでまとめて上げていきます。 以下が対象です。 androidx.appcompat:appcompat androidx.core:core-ktx androidx.constraintlayout:constraintlayout com.google.android.material:material androidx.fragment:fragment androidx.fragment:fragment-ktxに変更します androidx.recyclerview:recyclerview 以下はテスト関連です。テストの対応は後にしますが、一応今回の対応後もテストはそのままで通せるので、そのためにバージョンアップをしておきます。 androidx.test.ext:junit androidx.test:runner androidx.test:rules androidx.test.espresso:espresso-core androidx.test.espresso:espresso-contrib androidx.test.espresso:espresso-intents androidx.test:orchestrator androidx.test:core androidx.fragment:fragment-testing 以下はJetpackとは関係ないですが、テスト関連で連携するので一緒に上げます。 - org.robolectric:robolectric - 最新の4.5.1にアップします - org.assertj:assertj-core - 最新の3.19.0にアップします 1 build.gradleの更新 最終的に以下のようになりました。 app/build.gradle dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.fragment:fragment-ktx:1.3.2' implementation 'androidx.activity:activity-ktx:1.2.2' testImplementation 'junit:junit:4.13.2' testImplementation 'org.assertj:assertj-core:3.19.0' testImplementation 'androidx.test.ext:junit:1.1.2' testImplementation 'androidx.test:runner:1.3.0' testImplementation 'androidx.test:rules:1.3.0' testImplementation 'androidx.test.espresso:espresso-core:3.3.0' testImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' testImplementation 'androidx.test.espresso:espresso-intents:3.3.0' testImplementation 'org.robolectric:robolectric:4.5.1' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'org.assertj:assertj-core:3.19.0' androidTestUtil 'androidx.test:orchestrator:1.3.0' // 省略 // RecyclerView implementation 'androidx.recyclerview:recyclerview:1.2.0-rc01' // ViewPager2 implementation 'androidx.viewpager2:viewpager2:1.0.0' // 省略 // FragmentTest testImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:core:1.3.0' // Robolectric用,test向けではなくdebug向けに必要 debugImplementation 'androidx.fragment:fragment-testing:1.3.2' debugImplementation 'androidx.test:core:1.3.0' // 無くてもアプリの実行は問題ないがテストがビルド出来ない debugImplementation "androidx.legacy:legacy-support-core-ui:1.0.0" debugImplementation "androidx.legacy:legacy-support-core-utils:1.0.0" // 省略 } 今回、recyclerviewを除いて基本的にstable版のみ採用しています。build.gradleでショートカットキーとかで最新版にしようとするとβやα版が入ってしまいますが、以下の手順でやるとstable版のみ指定することができます。 Android StudioのProject Structureで、Modules-Dependenciesのappを選ぶと、以下のようにライブラリ毎にどんなバージョンがあるかリストで確認しながら設定していくことが出来ます。 2 deprecated警告に対応 以下の非推奨項目に警告が出ます。(出ていない場合はclean&rebuildしてみてください) 'onActivityCreated(Bundle?): Unit' is deprecated. 'startActivityForResult(Intent!, Int): Unit' is deprecated. 'onActivityResult(Int, Int, Intent?): Unit' is deprecated. 'setTargetFragment(Fragment?, Int): Unit' is deprecated. 'getter for targetFragment: Fragment?' is deprecated. 順番に対応していきましょう。 2.1 Fragment#onActivityCreatedのdeprecatedに対応する まずはドキュメントの非推奨になった理由などを確認します。 https://developer.android.com/jetpack/androidx/releases/fragment?hl=ja#1.3.0-alpha02 こちらに記載があります。 onActivityCreated() メソッドは非推奨になりました。フラグメントのビューをタッチするコードは onViewCreated()(onActivityCreated() の直前に呼び出される)、他の初期化コードは onCreate() で実行する必要があります。アクティビティの onCreate() が完了した際にコールバックを受け取るには、onAttach() のアクティビティの Lifecycle に LifeCycleObserver を登録し、onCreate() のコールバックを受け取ったら削除する必要があります。 (b/144309266) つまり onViewCreatedに処理を移すか、onAttachでLifeCycleObserverを登録してそのコールバックで実施せよ、とのことです。 今回は、onViewCreatedに初期化処理を移動する方法にします。 元のコード LogInputFragment.kt override fun onActivityCreated(view: View, savedInstanceState: Bundle?) { super.onActivityCreated(view, savedInstanceState) // 日付の選択を監視 viewModel.selectDate.observe(viewLifecycleOwner, Observer { binding.textDate.text = it.getDateStringYMD() }) // sns投稿設定 val shareStatus = viewModel.readShareStatus() binding.switchShare.isChecked = shareStatus.doPost binding.checkBoxTwitter.isChecked = shareStatus.postTwitter binding.checkBoxInstagram.isChecked = shareStatus.postInstagram } 対応後のコード LogInputFragment.kt override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) //...(省略) } オーバーライドする関数名とsuperクラスの関数名を変更しただけです。 アプリを実行して新規登録画面を操作してみても問題無さそうでした。 Activityのライフサイクルにもうちょっと厳密に従う必要がある場合を除いて、こちらの方法で良いのではないかなと思います。 2.2 startActivityForResultとonActivityResultのdeprecatedに対応する こちらのドキュメントによれば、 Activity Result API の統合: Activity 1.2.0 に導入された ActivityResultRegistry API に対するサポートを追加しました。これにより、startActivityForResult() + onActivityResult() と requestPermissions() + onRequestPermissionsResult() のフローを処理する際に Fragment でメソッドをオーバーライドする必要がなくなり、これらのフローをテストするためのフックを提供できるようになります。更新されたアクティビティからの結果の取得をご覧ください。 つまりActivity Result APIを使えとのことのようですね。 ただ、公式ドキュメントを読んでも結局どうしたらよいのか分からなかったので、こちらを参考にしました。 (1)依存ライブラリの追加 まず、androidx.activity:activity-ktxライブラリが必要になるのでbuild.gradleに追加します。 app/build.gradle dependencies { // 省略 implementation 'androidx.fragment:fragment-ktx:1.3.2' implementation 'androidx.activity:activity-ktx:1.2.2' // 追加 ... (2) 内部のActivityを起動して結果を受け取っているパターンの対応 先ほどご紹介したサイトの以下の項からが参考になります。 自身のアプリ内部で別Activityからの結果を受け取る 概要としては startActivityForResultの代わりにactivityResultLauncher.launchを使う onActivityResultの代わりにregisterForActivityResult(StartActivityForResult())でコールバックを登録しておく という手順に変わります。 例として、SignInActivity.ktを書き変えてみましょう。 元のコード SignInActivity.kt companion object { const val REQUEST_CODE_AUTH = 210 // (省略) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivitySigninBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.buttonSignIn.setOnClickListener { // ...(省略) // Create and launch sign-in intent startActivityForResult( authProvider.createSignInIntent(this), REQUEST_CODE_AUTH ) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_AUTH) { FirebaseCrashlytics.getInstance() .log("FirebaseUI Auth finished. result code = [$resultCode]") val response = IdpResponse.fromResultIntent(data) if (resultCode == Activity.RESULT_OK) { Log.d("AUTH", "Auth Completed.") // Successfully signed in analyticsUtil.sendSignInEvent() // TODO Roomのデータをコンバートしてアップロード // or Firestoreからデータをダウンロード } else response?.error?.errorCode?.let { errorCode -> analyticsUtil.sendSignInErrorEvent(errorCode) Log.d("AUTH", "Auth Error.") FirebaseCrashlytics.getInstance() .log("FirebaseUI Auth finished. error code = [$errorCode]") // Sign in failed. If response is null the user canceled the // sign-in flow using the back button. Otherwise check // response.getError().getErrorCode() and handle the error. // ... showError(errorCode) } } } 対応後のコード SignInActivity.kt import androidx.activity.result.ActivityResult // 追加 import androidx.activity.result.contract.ActivityResultContracts // 追加 class SignInActivity : BaseActivity() { companion object { // const val REQUEST_CODE_AUTH = 210 // 削除 // (省略) } // for ActivityResult API private val activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { onAuthProviderResult(it) } override fun onCreate(savedInstanceState: Bundle?) { // ... binding.buttonSignIn.setOnClickListener { // ... // Create and launch sign-in intent activityResultLauncher.launch(authProvider.createSignInIntent(this)) } } // onActivityResultは関数ごと削除 private fun onAuthProviderResult(result: ActivityResult) { FirebaseCrashlytics.getInstance() .log("FirebaseUI Auth finished. result code = [${result.resultCode}]") val response = IdpResponse.fromResultIntent(result.data) if (result.resultCode == Activity.RESULT_OK) { Log.d("AUTH", "Auth Completed.") // Successfully signed in analyticsUtil.sendSignInEvent() // TODO Roomのデータをコンバートしてアップロード // or Firestoreからデータをダウンロード } else response?.error?.errorCode?.let { errorCode -> analyticsUtil.sendSignInErrorEvent(errorCode) Log.d("AUTH", "Auth Error.") FirebaseCrashlytics.getInstance() .log("FirebaseUI Auth finished. error code = [$errorCode]") // Sign in failed. If response is null the user canceled the // sign-in flow using the back button. Otherwise check // response.getError().getErrorCode() and handle the error. // ... showError(errorCode) } } Activityの起動にrequestCodeが不要になる代わりに、同じ数だけregisterForActivityResultが必要になる感じかな。 onActivityResultがswicth分やif文で階層が深くなっていたのを防げる代わりに、private変数の乱立、というところとのトレードオフでしょうかねえ。 個人的には、リクエストコードの値を付けるのに無駄に悩んだりしていたので、それから解放されるのは嬉しいですねw (3) 暗黙的インテントなどで外部のアプリを起動して結果を受け取っているパターンの対応 本アプリではこのような処理はまだ入れていないものの、将来的に例えばInstagramに投稿する画像はギャラリーから選べるようにするなども考えているため、念のため調べようと思いましたが、こちらも先ほどのサイトが参考になりますのでそのリンクだけにしておきます。 アプリ外部から結果を受け取る 概要としては startActivityForResultの代わりにactivityResultLauncher.launchを使う onActivityResultの代わりにregisterForActivityResult(GetContent())でコールバックを登録しておく で、先ほどやった自分のアプリ内の別Activityを呼ぶパターンとそれほど違いは無さそうです。 暗黙的インテントの場合は、自分でIntentを作らなくても良くなるのかな。 この辺は、ActivityResultContractsのリファレンスなんかを読んでいくといろいろありそうでした。 前述の参考ページの例では、コンテンツのURIを取得したいのでActivityResultContracts.GetContentを使っており、適切なMIME-Typeを指定するとそれに応じた暗黙的インテントが投げられるのでしょう。 (4) FragmentからstartActivityしている場合 Fragment自身でstartActivityForResultをしてonActivityResultを受け取っている場合は、公式のサンプルにある通り、Fragment内でActivityResultAPIを使えば良いでしょう。 今回問題になったのは、Fragment内ではactivity.startActivityForResultしていて、結果はActivity#onActivityResultで処理している場合です。 サンプルにしているプロジェクトでは、MonthlyPageFragmentで起動し、MainAcvityで結果を受けていました。 MonthlyPageFragment.kt override fun onItemClick(data: CalendarCellData) { // 省略 activity?.startActivityForResult( intent, MainActivity.REQUEST_CODE_LOGITEM ) } MainActivity.kt override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { REQUEST_CODE_LOGITEM -> { onStepCountLogChanged(resultCode, data) return } // ...(省略) } こういうのはどうするのが良いんでしょうかね? 今回は安直に、MainActivity側にLogItemActivityを起動する関数を用意して呼ぶようにしました。 MainActivity.kt fun launchLogEditActivity(intent: Intent) { logItemActivityResultLauncher.launch(intent) } MonthlyPageFragment.kt (activity as MainActivity?)?.launchLogEditActivity(intent) 一応、Fragment#activity(getActivity)はnullableなため、nullチェックをしています。 (5) DIに対応する そのままでも良いのですが、Koinを使ってActivityResultRegistoryをDI可能にしておくと、テストで上手いこと使えそうなので、やってしまっておきましょう。 公式にはFragmentの場合ですが例(というかDIする場合のヒント?)があります。 (でも、Fragmentのコンストラクタに引数渡すのはNGじゃなかった??) https://developer.android.com/training/basics/intents/result?hl=ja#test Activityの場合はちょっとワザがいるので、自分でも事前にまとめていました。 ActivityResultAPIのテストを書く これに則ってやってみます。 (a) Koinにscopeモジュールを追加 Koinにモジュールを追加しますが、これまでとちょっと違います。 modules.kt // scopedモジュール群 val scopeModules = module { scope<MainActivity> { scoped { get<AppCompatActivity>().activityResultRegistry } } scope<SignInActivity> { scoped { get<AppCompatActivity>().activityResultRegistry } } } // モジュール群 val appModules = listOf( viewModelModule, daoModule, repositoryModule, providerModule, firebaseModule, scopeModules // 追加 ) appModulesへの追加もお忘れなく(やらかしたひとw) scopeというのは、SignInActivityの生存期間(スコープ)の間だけ生存する機能に対して使うことが出来ます。ただし、このモジュールを持つActivityは、ScopeActivityを継承している必要があります。 MainActivityやSignInActivityはAnalytics関連をまとめたBaseActivityを継承していますが、このBaseActivityをコピペしてSocpeBaseActivityとしました。 BaseAcvitiy.kt abstract class ScopeBaseActivity : ScopeActivity() { abstract val screenName: String // AnalyticsTool inject by Koin val analyticsUtil: AnalyticsUtilI by inject() override fun onResume() { super.onResume() analyticsUtil.sendScreenName(screenName) } } MainActivityとSignInActivityはこのScopeBaseActivityを基底クラスとするよう変更します。 MainActivity.kt class MainActivity : ScopeBaseActivity() { SignInActivityも同様です。 (b) registerForActivityResultの引数を変更する registerForActivityResult関数への第2引数get()に変更します。こうすることで、KoinがInjectionしてくれます。 例えばSignInActivityの場合です。 SignInActivity.kt // for ActivityResult API private val activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult(), get()) { onAuthProviderResult(it) } MainActivityも同様に3箇所全部、第2引数を追加します。 本体アプリへの対応は以上です。アプリを起動して動作させてみてください。 SignInActivityTestクラスやMainActivityTestIクラス(androidTest)も動くことを確認しておくと良いでしょう。 実際にテストでどうモックしていくかは、次回にやっていきます。 2.3 setTargetFragmentとgetTargetFragmentのdeprecatedに対応する こちらもActicityResultAPIと同じようにFragmentResultAPIを使うように変更します。 fragment1.3.0から導入されました。 公式ページはこちらです。 https://developer.android.com/guide/fragments/communicate#fragment-result その他の参考サイトはこちら。 https://qiita.com/TomAndDev/items/a7444d3ac6ef9d2d3ad4 該当しているクラスはConfirmDialogです。 このダイアログを使っているのはSignOutActivityです。 影響はこの2クラスですね。 (1) SignOutActivityのテストを追加する まず、テストが不足していたので追加しておきます。このクラスのアカウント削除確認に関する部分が、この変更に関係する部分なのです。 動作確認するのにアプリを実際に動かすより楽なので(何しろFirebase Authでのログインが絡んでしまうので)、この部分はテストでの対応をしていきます。 アカウント削除確認のダイアログのテスト SignOutActivityTest.kt @Test fun deleteAccount_data_converted() { // ResultActivityの起動を監視 val monitor = Instrumentation.ActivityMonitor( SignInActivity::class.java.canonicalName, null, false ) InstrumentationRegistry.getInstrumentation().addMonitor(monitor) activity = activityRule.launchActivity(null) onView(withId(R.id.signOutScroll)).perform(swipeUp()) // アカウント削除ボタン onView(withId(R.id.buttonAccountDelete)) .perform(scrollTo(), click()) onView(withText(R.string.confirm_account_delete_1)) .check(matches(isDisplayed())) onView(withText(R.string.label_yes)) .check(matches(isDisplayed())) .perform(click()) // コンバートしましたに「はい」と答えたので、アカウント削除をし自分は終了した Assertions.assertThat(activity.isFinishing).isEqualTo(true) // ResultActivityが起動したか確認 InstrumentationRegistry.getInstrumentation() .waitForMonitorWithTimeout(monitor, 1000L) Assertions.assertThat(monitor.hits).isEqualTo(1) } @Test fun deleteAccount_anyway() { // ResultActivityの起動を監視 val monitor = Instrumentation.ActivityMonitor( SignInActivity::class.java.canonicalName, null, false ) InstrumentationRegistry.getInstrumentation().addMonitor(monitor) activity = activityRule.launchActivity(null) onView(withId(R.id.signOutScroll)).perform(swipeUp()) // アカウント削除ボタン onView(withId(R.id.buttonAccountDelete)) .perform(scrollTo(), click()) onView(withText(R.string.confirm_account_delete_1)) .check(matches(isDisplayed())) onView(withText(R.string.label_no)) .check(matches(isDisplayed())) .perform(click()) onView(withText(R.string.confirm_account_delete_2)) .check(matches(isDisplayed())) onView(withText(R.string.label_yes)) .check(matches(isDisplayed())) .perform(click()) // アカウント削除をし自分は終了した Assertions.assertThat(activity.isFinishing).isEqualTo(true) // ResultActivityが起動したか確認 InstrumentationRegistry.getInstrumentation() .waitForMonitorWithTimeout(monitor, 1000L) Assertions.assertThat(monitor.hits).isEqualTo(1) } (2) ConfirmDialogをFragmentから起動するテストを書く ConfirmDialogはアプリ内では現在Activityからしか使っていません。Fragmentが使う場合の確認が出来ないので、テストに書いておくことにします。 ConfirmDialogのテスト ConfirmDialogTest.kt package jp.les.kasa.sample.mykotlinapp.alert import android.content.DialogInterface import android.os.Bundle import androidx.fragment.app.Fragment import androidx.fragment.app.testing.launchFragmentInContainer import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers import jp.les.kasa.sample.mykotlinapp.R import org.assertj.core.api.Assertions.assertThat import org.junit.Test // 本体アプリではConfirmDialogがFragmentから参照されていないので、テスト用に作る class SampleFragment : Fragment(), ConfirmDialog.ConfirmEventListener { fun showConfirm() { val dialog: ConfirmDialog = ConfirmDialog.Builder() .target(this).requestCode(100) .message("てすと").create() dialog.show(requireFragmentManager(), "tag") } var confirmResult: Boolean? = null override fun onConfirmResult(which: Int, bundle: Bundle?, requestCode: Int) { confirmResult = when (which) { DialogInterface.BUTTON_POSITIVE -> true else -> false } } } class ConfirmDialogTest { private lateinit var fragment: SampleFragment @Test fun showFromFragment() { val scenario = launchFragmentInContainer<SampleFragment>(themeResId = R.style.AppTheme) scenario.onFragment { fragment = it it.showConfirm() } // Dialogが表示されている? Espresso.onView(ViewMatchers.withText("てすと")) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) Espresso.onView(ViewMatchers.withText(R.string.label_yes)) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) Espresso.onView(ViewMatchers.withText(R.string.label_no)) .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) assertThat(fragment.confirmResult).isNull() } @Test fun cancelDialog(){ val scenario = launchFragmentInContainer<SampleFragment>(themeResId = R.style.AppTheme) scenario.onFragment { fragment = it it.showConfirm() } Espresso.onView(ViewMatchers.withText(R.string.label_no)) .perform(click()) Espresso.onView(ViewMatchers.withText("てすと")) .check(doesNotExist()) assertThat(fragment.confirmResult).isEqualTo(false) } @Test fun confirmDialog(){ val scenario = launchFragmentInContainer<SampleFragment>(themeResId = R.style.AppTheme) scenario.onFragment { fragment = it it.showConfirm() } Espresso.onView(ViewMatchers.withText(R.string.label_yes)) .perform(click()) Espresso.onView(ViewMatchers.withText("てすと")) .check(doesNotExist()) assertThat(fragment.confirmResult).isEqualTo(true) } } 概要としては、テスト用のSampleFragmentを作って、そいつがConfirmDialogを表示して結果を受け取る処理を単純なコードでいいので作っておきます。テストしやすいように全部publicです。 テストでは、FragmentScenarioを使ってFragmentを仮の環境の中で起動させます。このとき、IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activityというエラーが出る場合、launchFragmentInContainerの引数でテーマを指定しておくと良いようです。 val scenario = launchFragmentInContainer<SampleFragment>(themeResId = R.style.AppTheme) 参考サイト: https://stackoverflow.com/questions/32346748/robolectric-illegalstateexception-you-need-to-use-a-theme-appcompat-theme-or (3) ConfirmDialogクラスの変更 setTargeFragmentしたりgetTargetFragmentしているのをFragmentResultAPIに置き換えていきます。 (a) リスナーを削除 不要になるのでConfirmEventListenerを削除してしまいます。 (b) ConfirmDialog#Builderクラスを変更 target, requestCode, それからdata関数も不要になるので削除します。 これでsetTargetFragmentが削除出来ます。 (c) show関数をオーバーロードする 参考サイトの通りに、show関数のオーバーロードを用意してコールバックをラムダで渡せるようにします。 (※関数のオーバーロードとは、引数が違う同名の関数を定義すること) Activityから呼ばれる場合と、Fragmentから呼ばれる場合とで微妙に違うので関数も分けました。 ConfirmDialog.kt companion object { // ... const val TAG = "ConfirmDialog" const val REQUEST_KEY = "confirmDialog" const val RESULT_KEY_NEGATIVE = "confirmDialogNegative" const val RESULT_KEY_POSITIVE = "confirmDialogPositive" } fun show( activity: AppCompatActivity, onPositive: (() -> Unit)? = null, onNegative: (() -> Unit)? = null ) { activity.supportFragmentManager.setFragmentResultListener(REQUEST_KEY, activity) { requestKey, bundle -> if (requestKey != REQUEST_KEY) return@setFragmentResultListener when { bundle.containsKey(RESULT_KEY_NEGATIVE) -> onNegative?.invoke() bundle.containsKey(RESULT_KEY_POSITIVE) -> onPositive?.invoke() } } show(activity.supportFragmentManager, TAG) } fun show( target: Fragment, onPositive: (() -> Unit)? = null, onNegative: (() -> Unit)? = null ) { target.childFragmentManager.setFragmentResultListener( REQUEST_KEY, target.viewLifecycleOwner ) { requestKey, bundle -> if (requestKey != REQUEST_KEY) return@setFragmentResultListener when { bundle.containsKey(RESULT_KEY_NEGATIVE) -> onNegative?.invoke() bundle.containsKey(RESULT_KEY_POSITIVE) -> onPositive?.invoke() } } show(target.childFragmentManager, TAG) } それぞれ、対象のFragmentManagerに対してsetFragmentResultListenerをしています。 リクエストキーREQUEST_KEYに対して、結果のBundleに含まれるキーに応じて、コールバックを呼んでいます。 return@setFragmentResultListenerの部分についてですが、これは setFragmentResultListenerというラベルが付いたラムダを抜ける というのを意味しています。 ラベルというのは、一部の言語にあるgoto文のラベルみたいなもので、こんな風にラベルを付けることが出来るのがKotlinの仕様になっています。 fun foo(){ bar @label { return@label } } ただ、暗黙のラベルというのがあって、それは「ラムダが渡される関数の名前」ということになっています。今回setFragmentResultListenerに対して渡しているラムダなので、そのラムダを抜けるのには暗黙のラベルを使ってreturn@setFragmentResultListenerとすることが出来る、というわけです。することが出来ると言うより、しなければならない、ですかね。ラベルがないとAndroid Studioが警告を出します。 単なるreturnだと、普通は「関数」を抜けることを意味します。ある関数の中でラムダを呼んでいたら、ラムダを呼んでいる関数自体を抜けてしまいます。 リターン、ラベルについては、以下のページの説明がわかりやすかったので参考にしてみて下さい。 https://qiita.com/k5n/items/acfaff8b56faf57971f7#%E3%83%AA%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%A8%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%97 (d) 結果のセット FragmentResultListenerに結果をセットする場所は、onClickになります。ここでsetFragmentResultをすると、登録したリスナーにコールバックされていきます。 ConfirmDialog.kt override fun onClick(dialog: DialogInterface?, which: Int) { FirebaseCrashlytics.getInstance().log("ConfirmDialog selected:$which") when (which) { DialogInterface.BUTTON_POSITIVE -> setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_POSITIVE to true)) else -> setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_NEGATIVE to true)) } } POSITIVEボタンだったらリザルトキーRESULT_KEY_POSITIVEにtrueを、それ以外だったらRESULT_KEY_NEGATIVEにtrue、というようにBundleを作成して結果にセットしています。 bundleOfというのは、 Bundle().apply { putXXXX(key, value) } とやっているのと同義です。私も参考サイトを見ていて知りました。 androidx.core.os.BundleKt.classで定義されている関数です。 実装を見てみるとかなりの力技な気もしますが ConfirmDialogの変更は以上です。 (4) SignOutActivityの変更 SignOutActivityを変更していきます。まず、ConfirmEventListenerのimplementsが不要になったので削除します。 onConfirmResult関数が不要になり、それぞれ以下のような関数にしてConfirmDialogの呼び出しとコールバックの処理を書いていくと、スッキリするのではないでしょうか。 SignOutActivity.kt override fun onCreate(savedInstanceState: Bundle?) { // ... // アカウント削除ボタン binding.buttonAccountDelete.setOnClickListener { analyticsUtil.sendButtonEvent("delete_account") // 確認フローを開始する showDeleteAccountConfirm() } } private fun showDeleteAccountConfirm() { val dialog = ConfirmDialog.Builder() .message(R.string.confirm_account_delete_1) .create() dialog.show(this, onPositive = { // データ削除した、なので削除決行 doDeleteAccount() }, onNegative = { // データ削除しなくてよいかもう一度確認 showDeleteAccountConfirmLast() }) } private fun showDeleteAccountConfirmLast() { // データ削除しなくてよいかもう一度確認 val dialog = ConfirmDialog.Builder() .message(R.string.confirm_account_delete_2) .create() dialog.show(this, onPositive = { // アカウント削除してよい、なので削除決行 doDeleteAccount() }, onNegative = { // いいえなので何もしない }) } (5) SampleFragmentの変更 こちらはテストコードの変更になります。 SampleFragmentは以下のようになりました。 ConfirmDialogTest.kt class SampleFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val view = TextView(context) view.text = "HELLO" return view } fun showConfirm() { val dialog: ConfirmDialog = ConfirmDialog.Builder() .message("てすと").create() dialog.show( this, onPositive = { confirmResult = true }, onNegative = { confirmResult = false }) } var confirmResult: Boolean? = null } onCreateViewでViewを何かしら返さないとviewLifecycleOwnerが作成されないらしく、テスト実行時にエラーになったのでとりあえずTextViewを作って返すようにしました。 (ViewがないならlifecycleOwnerは不要でしょってことでしょうかね) あとはSignOutActivityで対応したのとほぼ同じことをするだけです。 (6) テストを実行して確認する 先ほど(1)と(2)で追加したテストを流してみて、パスすれば完了です。 2.4 Koinモジュール定義に出る警告no cast neededに対応する Koinのアップデートを前回行い、以下のような警告が出るようになっていました。 > Task :app:compileDebugKotlin w: /Users/sachie/workspace/github/qiita_pedometer/app/src/main/java/jp/les/kasa/sample/mykotlinapp/di/modules.kt: (47, 34): No cast needed w: /Users/sachie/workspace/github/qiita_pedometer/app/src/main/java/jp/les/kasa/sample/mykotlinapp/di/modules.kt: (48, 37): No cast needed w: /Users/sachie/workspace/github/qiita_pedometer/app/src/main/java/jp/les/kasa/sample/mykotlinapp/di/modules.kt: (53, 50): No cast needed w: /Users/sachie/workspace/github/qiita_pedometer/app/src/main/java/jp/les/kasa/sample/mykotlinapp/di/modules.kt: (54, 49): No cast needed が、 なお、以下のように「キャストが不要だというエラーが出ますが、この部分のキャストを消すとKoinのInjectに不具合が生じるので、消してはいけません。 といって特に対応していませんでした。 ところが、以下によると、警告を消すことが出来るようなので対応しておきます。 対応前はこうなっていたのを、 modules.kt val providerModule = module { factory { CalendarProvider() as CalendarProviderI } factory { EnvironmentProvider() as EnvironmentProviderI } } // FirebaseService val firebaseModule = module { single { AnalyticsUtil(androidApplication()) as AnalyticsUtilI } single { AuthProvider(androidApplication()) as AuthProviderI } } 以下のようにするだけです。 modules.kt val providerModule = module { factory<CalendarProviderI> { CalendarProvider() } factory<EnvironmentProviderI> { EnvironmentProvider() } } // FirebaseService val firebaseModule = module { single<AnalyticsUtilI> { AnalyticsUtil(androidApplication()) } single<AuthProviderI> { AuthProvider(androidApplication())} } as XXXXによるキャストをやめて、ジェネリックの型パラメータに変更です。 テストの方のモックモジュールも対応しておきましょう。 ここまでで、アプリの実行、テストの実行がそれぞれ問題ないことを確認出来れば今回の作業は完了です。 まとめ Android Jetpack関連のバージョンをstable版の最新(2021/03/01現在)に更新しました。 非推奨となった処理の代替として、Activity Result APIとFragment Result APIに対応しました。 ここまでのコードは以下にアップしてあります。 https://github.com/le-kamba/qiita_pedometer/tree/feature/qiita_16 次回予告 今回対応したJetpackのバージョンアップに伴う、テストの修正を行っていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む