ViewModel はいつ生まれていつ死ぬか 【→ Jetpack Compose】

ViewModel は onCleared() を自分が破棄されるときに実行します。


public abstract class ViewModel {

    /**
     * This method will be called when this ViewModel is no longer used and will be destroyed.
     * It is useful when ViewModel observes some data and you need to clear this subscription to
     * prevent a leak of this ViewModel.
     */
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }

This method will be called when this ViewModel is no longer used and will be destroyed.

👉 ViewModel の概要  |  Android デベロッパー  |  Android Developers hatena-bookmark

ログを吐かせて、その瞬間を確認します。

初期化のタイミングと合わせて確認してみましょう。


@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel() {

  // ...

  init {
    Timber.d("## ${javaClass.simpleName} initialized.")
  }

  override fun onCleared() {
    Timber.d("## ${javaClass.simpleName} onCleared.")
  }
}

👉 JakeWharton/timber: A logger with a small, extensible API which provides utility on top of Android's normal Log class. hatena-bookmark


...
21077-21077/com.example.app D/HomeViewModel: ## HomeViewModel initialized.
...
21077-21077/com.example.app D/HomeViewModel: ## HomeViewModel onCleared.
...

なんでいまさらこんなことを!

(つづく...)

👉 【Jetpack Compose】ViewModel を捨てて Repository を Composable に直結する hatena-bookmark
👉 Compose を既存のアプリ アーキテクチャと統合する  |  Jetpack Compose  |  Android Developers hatena-bookmark


【Dagger-Hilt】「androidx.hilt:hilt-lifecycle-viewmodel」を使ってはならない Dagger 2.34+

The Hilt Android Gradle plugin is applied but no com.google.dagger:hilt-android dependency was found.

The Hilt Android Gradle plugin is applied but no com.google.dagger:hilt-android dependency was found.

すいません、またハマっちゃいました、2回目です。

原因は以下なのですが。

androidx.hilt:hilt-lifecycle-viewmodel artifacts were deprecated in the Dagger 2.34 release in favor of native Hilt API.

👉 ComponentProcessingStep was unable to process '*Application_HiltComponents.SingletonC' · Issue #3257 · google/dagger hatena-bookmark

Migration steps:
...
4. Remove the old androidx.hilt:hilt-lifecycle-viewmodel dependency from your build.gradle file

👉 Release Dagger 2.34 · google/dagger hatena-bookmark

以下があると、ビルドでコケます。


implementation 'androidx.hilt:hilt-lifecycle-viewmodel:x.y.z'

知ってます、みんなハマっています。

 

対応方法

これでいけます。


dependencies {

  //implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
  //kapt 'androidx.hilt:hilt-compiler:1.0.0'

  implementation 'com.google.dagger:hilt-android:2.42'
  kapt 'com.google.dagger:hilt-compiler:2.42'

androidx.hilt.* だけでは、ビルドできないか、インストール→起動後に落ちます。まだあやしい。

androidx.hilt:hilt-lifecycle-viewmodel

 

なぜ、ハマるのか

公式リファレンスに記述があるんですよね、この記述。

Hilt と Jetpack の統合  |  Android デベロッパー  |  Android Developers

👉 Hilt と Jetpack の統合  |  Android デベロッパー  |  Android Developers hatena-bookmark

Hilt に隠れて Dagger のバージョンが見えづらくなってることにも原因があるように思います。

👉 Error: ComponentProcessingStep was unable to process 'AppApplication_HiltComponents.SingletonC' because 'DefaultActivityViewModelFactory' could not be resolved. hatena-bookmark


利用している Google Mobile Ads SDK (play-services-ads) のバージョンを確認する

👉 Google Play と Android の変更点に対応するためのアプリの準備 - Google AdMob ヘルプ hatena-bookmark


implementation com.google.android.gms:play-services-ads:x.y.z

20.4.0 (バージョン)
→ 広告IDの使用を継続するために自動的に権限が宣言されます。

20.5.0
→ オプトアウト済みユーザーのデータ収集と不正防止に対応するため、新しいアプリアセットIDに対応します。

20.6.0
→ tagForChildDirected(TFCD)または tagForUnderAgeOfConsent(TFUA)を通して子供向け取り扱いタグが付与された広告リクエストにおいては、広告IDが送信されないようにすることができます。

👉 Googleモバイル広告SDK  |  Android  |  Google Developers hatena-bookmark

現在、アプリで利用中のバージョンを build.gradle で確認してみたら、


com.google.android.gms:play-services-ads

がありません!

 

依存性を確認していく


./gradlew -q :app:androidDependencies  

...
release
releaseCompileClasspath - Dependencies for compilation
...
+--- com.google.android.play:core:1.10.3@aar
+--- com.google.android.gms:play-services-analytics:18.0.1@aar
+--- com.google.android.gms:play-services-oss-licenses:17.0.0@aar
+--- com.google.android.gms:play-services-tagmanager-v4-impl:18.0.1@aar
+--- com.google.android.gms:play-services-analytics-impl:18.0.1@aar
+--- com.google.firebase:firebase-messaging-ktx:23.0.4@aar
+--- com.google.firebase:firebase-messaging:23.0.4@aar
+--- com.firebaseui:firebase-ui-auth:8.0.1@aar
+--- com.google.android.gms:play-services-auth:19.0.0@aar
+--- com.google.firebase:firebase-ads:20.6.0@aar
+--- com.google.android.gms:play-services-ads:20.6.0@aar 👈
+--- com.google.android.gms:play-services-appset:16.0.0@aar
+--- com.google.firebase:firebase-auth-ktx:21.0.3@aar
+--- com.google.firebase:firebase-auth:21.0.3@aar
+--- com.google.android.gms:play-services-auth-api-phone:17.4.0@aar
+--- com.google.android.gms:play-services-safetynet:17.0.0@aar
+--- com.google.android.gms:play-services-auth-base:17.0.0@aar
+--- com.google.android.gms:play-services-base:18.0.1@aar
...

play-services-ads は、20.6.0 が、なぜか、見えますが。

さらに詳細を確認していきます。


./gradlew -q :app:dependencies --configuration releaseCompileClasspath

...
|         |         +--- com.google.android.gms:play-services-basement:17.0.0 -> 18.0.2 (*)
|         |         \--- com.google.firebase:firebase-annotations:16.0.0
|         \--- com.google.android.gms:play-services-measurement-sdk:21.0.0
|              +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
|              +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.0.2 (*)
|              +--- com.google.android.gms:play-services-measurement-base:21.0.0 (*)
|              \--- com.google.android.gms:play-services-measurement-impl:21.0.0 (*)
+--- com.google.firebase:firebase-ads:20.6.0 👈
|    +--- com.google.android.gms:play-services-ads:20.6.0 👈
|    |    +--- androidx.browser:browser:1.0.0 -> 1.3.0
|    |    |    +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
|    |    |    +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|    |    |    \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava
|    |    +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
|    |    +--- androidx.core:core:1.0.0 -> 1.7.0 (*)
|    |    +--- com.google.android.gms:play-services-ads-base:20.6.0
...
(c) - dependency constraint
(*) - dependencies omitted (listed previously)
(n) - Not resolved (configuration is not meant to be resolved)

dependencies の記述の親子依存の調べ方

firebase-ads に依存されていますね。

もう一度、build.gradle を見てみます。


dependencies {
  ...
  implementation 'com.google.firebase:firebase-core:21.0.0'
  implementation 'com.google.firebase:firebase-ads:20.6.0' 👈
  implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
  ...

つまりは、

play-services-ads の implementation 記述がなくても

firebase-ads から依存を追って解決されていってたのですね!!


This version (1.2.0-alpha08) of the Compose Compiler requires Kotlin version 1.6.20 but you appear to be using Kotlin version 1.6.21 which is not known to be compatible.

こんなの出ましたけどー。


> Run with --scan to get full insights.


> Task :app:compileDebugKotlin FAILED
e: This version (1.2.0-alpha08) of the Compose Compiler requires Kotlin version 1.6.20 but you appear to be using Kotlin version 1.6.21 which is not known to be compatible.  Please fix your configuration (or `suppressKotlinVersionCompatibilityCheck` but don't say I didn't warn you!).

言われるがままに、kotlin のバージョンを、1.6.20 に下げればこのエラーは消えてビルド通りますが、

Kotlin 安定版の 1.6.21 を 1.6.20 を下げる

ってのどうなのと。

 

Compose コンパイラ と Kotlin の互換性


Compose Compiler	Compatible Kotlin
1.2.0-alpha08		1.6.20
1.2.0-alpha07		1.6.10
1.2.0-alpha06		1.6.10

以下から、英語版 (言語を「English」に切り替える) にすると分かりやすいです。

👉 Compose to Kotlin Compatibility Map  |  Android Developers hatena-bookmark

Kotlin stable 1.6.21 に対応する Compose Compiler は 今現在は pre-release 版しかありません。


 

何でこんなことになるのか?


通常、安定版とアルファ版の両方が同じ日に修正された状態でリリースされると思われます。しかし、AndroidXのモノレポとその関連インフラは、実はこれを阻んでいます。リリースブランチが一つしかないので、Accompanist のような通常のプロジェクトとは異なり、別々に行うことを余儀なくされるのです。


Kotlin 1.6.20のアーティファクトがリリースされてから1週間。ComposeをアップデートするCLはリリース前にできていた。しかし、AndroidXのモノレポでは、無関係な50のライブラリを同時にアップデートすることを強制されるため、未だに不完全です。健全なプロジェクトであれば、同じ日にリリースするはずです。

Drop "androidx" from Jetpack Compose package name, for multiplatform, before 1​.​0 release.
👉 キャンペーン · Drop "androidx" from Jetpack Compose package name, for multiplatform, before 1​.​0 release. · Change.org hatena-bookmark

Compose コンパイラを開発する環境にいろいろ問題とかあるんですかね。

👉 公式 android デベロッパー の英語ページに日本語ページが遅れてる件 hatena-bookmark

 

2022-05-21 追記

1.6.21 対応の beta版がリリースされています。

compose の父が!


これも「English」ページのみ確認できます。注意です!
kotlinCompilerExtensionVersion = "1.2.0-beta02"

👉 Compose to Kotlin Compatibility Map  |  Android Developers hatena-bookmark



【Kotlin】Flow の挙動やライフサイクルをログで確認する

Flow って、生きてるのか、死んでるのか、何が流れているのか、とか、慣れるまでは分かりづらいですね!


👉 【Kotlin】ぼくらは Flow の マーブルダイアグラム を見るのか。 hatena-bookmark

 

◾ 各タイミングでログを吐かせて確認する

Flow の中間オペレーターである onStart() や onEach() などを利用してログを吐かせて流れを確認します。

onStart()

collect を開始する前に、与えられたアクションを呼び出す Flow を返します。

onEach()

上流側 Flow からのそれぞれの値が下流へ emit される前に指定されたアクションを実行します。

onCompletion()

Flow が完了(またはキャンセル)された後に指定されたアクションを呼び出します。
Flow を返し、アクションの原因のパラメーターとして、キャンセル、例外、または失敗を渡します。

onEmpty()

Flow が要素を emit せずに完了したとき、指定されたアクションを実行します。

onSubscription()

この SharedFlow が collect を開始した(サブスクリプション)後、与えられたアクションを呼び出す Flow を返します。

まとめて整理して、エクステンションにしておきます。

 

■ 実際にやってみる

よくある flatMapLatest を使った RecyclerView で利用するアイテムのリスト呼び出しで実際にやってみます。


private val pokemonFetchingIndex: MutableStateFlow<Int> = MutableStateFlow(0)

private val pokemonListFlow = pokemonFetchingIndex
  .log("INDEX") // ADDED
  .flatMapLatest { page ->
    mainRepository.fetchPokemonList(
      page = page,
      onStart = { isLoading = true },
      onComplete = { isLoading = false },
      onError = { toastMessage = it }
    ).log("LIST") // ADDED
  }

@MainThread
fun fetchNextPokemonList() {
  if (!isLoading) {
    pokemonFetchingIndex.value++
  }
}

pokedex:MainViewModel.kt#L47-L54

以下のようにログが出力され、ホットな Flow である[INDEX]が変化するたびに、flatMapLatest ブロック内の Flow [LIST]が onStart → onEach → onCompletion と流れていく様子が分かります。


19:54:52.779 D: ### [INDEX] StateFlowImpl onStart()
19:54:52.781 D: ### [INDEX] StateFlowImpl onEach() value = 0
19:54:52.788 D: ### [LIST] ChannelFlowOperatorImpl onStart()
19:54:53.354 D: ### [LIST] ChannelFlowOperatorImpl onEach() value = [Pokemon(page=0, name=bulbasaur, url=https://pokeapi.co/api/v2/pokemon/1/), Pokemon(page=0, name=ivysaur, url=https://pokeapi.co/api/v2/pokemon/2/), Pokemon(page=0, name=venusaur, url=https://pokeapi.co/api/v2/pokemon/3/), Pokemon(page=0, name=charmander, url=https://pokeapi.co/api/v2/pokemon/4/), Pokemon(page=0, name=charmeleon, url=https://pokeapi.co/api/v2/pokemon/5/), Pokemon(page=0, name=charizard, url=https://pokeapi.co/api/v2/pokemon/6/), Pokemon(page=0, name=squirtle, url=https://pokeapi.co/api/v2/pokemon/7/), Pokemon(page=0, name=wartortle, url=https://pokeapi.co/api/v2/pokemon/8/), Pokemon(page=0, name=blastoise, url=https://pokeapi.co/api/v2/pokemon/9/), Pokemon(page=0, name=caterpie, url=https://pokeapi.co/api/v2/pokemon/10/), Pokemon(page=0, name=metapod, url=https://pokeapi.co/api/v2/pokemon/11/), Pokemon(page=0, name=butterfree, url=https://pokeapi.co/api/v2/pokemon/12/), Pokemon(page=0, name=weedle, url=https://pokeapi.co/api/v2/pokemon/13/), Pokemon(page=0, name=kakuna, url=https://pokeapi.co/api/v2/pokemon/14/), Pokemon(page=0, name=beedrill, url=https://pokeapi.co/api/v2/pokemon/15/), Pokemon(page=0, name=pidgey, url=https://pokeapi.co/api/v2/pokemon/16/), Pokemon(page=0, name=pidgeotto, url=https://pokeapi.co/api/v2/pokemon/17/), Pokemon(page=0, name=pidgeot, url=https://pokeapi.co/api/v2/pokemon/18/), Pokemon(page=0, name=rattata, url=https://pokeapi.co/api/v2/pokemon/19/), Pokemon(page=0, name=raticate, url=https://pokeapi.co/api/v2/pokemon/20/)]
19:54:53.362 D: ### [LIST] ChannelFlowOperatorImpl onCompletion() cause = null
19:54:54.553 D: ### [INDEX] StateFlowImpl onEach() value = 1
19:54:54.556 D: ### [LIST] ChannelFlowOperatorImpl onStart()
19:54:54.855 D: ### [LIST] ChannelFlowOperatorImpl onEach() value = [Pokemon(page=0, name=bulbasaur, url=https://pokeapi.co/api/v2/pokemon/1/), Pokemon(page=0, name=ivysaur, url=https://pokeapi.co/api/v2/pokemon/2/), Pokemon(page=0, name=venusaur, url=https://pokeapi.co/api/v2/pokemon/3/), Pokemon(page=0, name=charmander, url=https://pokeapi.co/api/v2/pokemon/4/), Pokemon(page=0, name=charmeleon, url=https://pokeapi.co/api/v2/pokemon/5/), Pokemon(page=0, name=charizard, url=https://pokeapi.co/api/v2/pokemon/6/), Pokemon(page=0, name=squirtle, url=https://pokeapi.co/api/v2/pokemon/7/), Pokemon(page=0, name=wartortle, url=https://pokeapi.co/api/v2/pokemon/8/), Pokemon(page=0, name=blastoise, url=https://pokeapi.co/api/v2/pokemon/9/), Pokemon(page=0, name=caterpie, url=https://pokeapi.co/api/v2/pokemon/10/), Pokemon(page=0, name=metapod, url=https://pokeapi.co/api/v2/pokemon/11/), Pokemon(page=0, name=butterfree, url=https://pokeapi.co/api/v2/pokemon/12/), Pokemon(page=0, name=weedle, url=https://pokeapi.co/api/v2/pokemon/13/), Pokemon(page=0, name=kakuna, url=https://pokeapi.co/api/v2/pokemon/14/), Pokemon(page=0, name=beedrill, url=https://pokeapi.co/api/v2/pokemon/15/), Pokemon(page=0, name=pidgey, url=https://pokeapi.co/api/v2/pokemon/16/), Pokemon(page=0, name=pidgeotto, url=https://pokeapi.co/api/v2/pokemon/17/), Pokemon(page=0, name=pidgeot, url=https://pokeapi.co/api/v2/pokemon/18/), Pokemon(page=0, name=rattata, url=https://pokeapi.co/api/v2/pokemon/19/), Pokemon(page=0, name=raticate, url=https://pokeapi.co/api/v2/pokemon/20/), Pokemon(page=1, name=spearow, url=https://pokeapi.co/api/v2/pokemon/21/), Pokemon(page=1, name=fearow, url=https://pokeapi.co/api/v2/pokemon/22/), Pokemon(page=1, name=ekans, url=https://pokeapi.co/api/v2/pokemon/23/), Pokemon(page=1, name=arbok, url=https://pokeapi.co/api/v2/pokemon/24/), Pokemon(page=1, name=pikachu, url=https://pokeapi.co/api/v2/pokemon/25/), Pokemon(page=1, name=raichu, url=https://pokeapi.co/api/v2/pokemon/26/), Pokemon(page=1, name=sandshrew, url=https://pokeapi.co/api/v2/pokemon/27/), Pokemon(page=1, name=sandslash, url=https://pokeapi.co/api/v2/pokemon/28/), Pokemon(page=1, name=nidoran-f, url=https://pokeapi.co/api/v2/pokemon/29/), Pokemon(page=1, name=nidorina, url=https://pokeapi.co/api/v2/pokemon/30/), Pokemon(page=1, name=nidoqueen, url=https://pokeapi.co/api/v2/pokemon/31/), Pokemon(page=1, name=nidoran-m, url=https://pokeapi.co/api/v2/pokemon/32/), Pokemon(page=1, name=nidorino, url=https://pokeapi.co/api/v2/pokemon/33/), Pokemon(page=1, name=nidoking, url=https://pokeapi.co/api/v2/pokemon/34/), Pokemon(page=1, name=clefairy, url=https://pokeapi.co/api/v2/pokemon/35/), Pokemon(page=1, name=clefable, url=https://pokeapi.co/api/v2/pokemon/36/), Pokemon(page=1, name=vulpix, url=https://pokeapi.co/api/v2/pokemon/37/), Pokemon(page=1, name=ninetales, url=https://pokeapi.co/api/v2/pokemon/38/), Pokemon(page=1, name=jigglypuff, url=https://pokeapi.co/api/v2/pokemon/39/), Pokemon(page=1, name=wigglytuff, url=https://pokeapi.co/api/v2/pokemon/40/)]
19:54:54.872 D: ### [LIST] ChannelFlowOperatorImpl onCompletion() cause = null

👉 【MVVM】 Kotlin Flow で使える5つの利用パターン hatena-bookmark
👉 Playing with Kotlin Flows. Nowadays we are listening to words… | by Davide Cerbo | Medium hatena-bookmark
👉 skydoves/Pokedex: 🗡️ Pokedex demonstrates modern Android development with Hilt, Material Motion, Coroutines, Flow, Jetpack (Room, ViewModel) based on MVVM architecture. hatena-bookmark