SparseArray error: Call requires API level S : SparseArray.set()【Kotlin】

こんなの出ましたけど。

SparseArray error: Call requires API level S : SparseArray.set()

どうしましょう。

また、アノテーションつけて、SDKバージョンによる分岐ですか?

内容

API level 1 からある SparseArray ですが、set メソッドが API level 31 (Android 12/S) で追加されています。

Alias for put(int, java.lang.Object) to support Kotlin [index]= operator.

👉 SparseArray  |  Android Developers 

set was added in Android 12 and is just a copy of the put function, which you can freely use instead.

setはAndroid12で追加されたもので、関数の単なるコピーであり、put代わりに自由に使用できます。

The reason they copied the function with a different name is so array access syntax can work in Kotlin. When the Kotlin compiler sees a function in a Java class with the name set and two parameters, it allows it to be called using array syntax like this:

配列アクセスがKotlinで機能できるようにするためです。Kotlinコンパイラが名前setと2つのパラメータを持つJavaクラスの関数を検出すると、次のような配列構文を使用して関数を呼び出すことができます。


mySparseArray.put(3, someObject)

// is the same as
mySparseArray[3] = someObject

But since they only added it in Android 12, it is not practical to use the new function if you are targeting anything less than Android 12 (sdk version 31), so it will take a few years to become useful.

ただし、Android 12でのみ追加されているため、Android 12(sdkバージョン31)未満をターゲットにしている場合は、新しい機能を使用するのは実用的ではありません。そのため、使用できるようになるまでに数年かかります。

In my opinion, it was a mistake to add this function to the base class, because now it is impossible to use array access syntax for a few years until it finally becomes sensible to make SDK 31 the minSdkVersion. Before they added it, it was possible to use an extension function to have this functionality, but now an extension function cannot be used because its name would shadow the one in the base class. Also, lint doesn't show an error for the array access syntax, so it will crash at runtime if you use it. They should have put this in as an extension function in androidx-core-ktx.

私の意見では、この関数を基本クラスに追加するのは間違いでした。SDK31を最終的に実用化するまで、数年間は配列アクセス構文を使用できなくなったためminSdkVersionです。追加する前は、拡張関数を使用してこの機能を使用することは可能でしたが、その名前が基本クラスの拡張関数をシャドウするため、拡張関数を使用できなくなりました。また、lintは配列アクセス構文のエラーを表示しないため、使用すると実行時にクラッシュします。彼らはこれを拡張関数として androidx-core-ktx に入れるべきでした。

Edit: Looks like maybe this is fixed in an upcoming version, if I'm understanding "backport" correctly.

編集:「バックポート」を正しく理解していれば、これは今後のバージョンで修正される可能性があります。

👉 android - SparseArray error: Call requires API level S - Stack Overflow 

The core-ktx library defines extension functionSparseArray.set(). This method does not exist in the Android SDK until S, the currently in-development version. Many apps use this extension function and compile against pre-S SDKs, meaning it resolves to the library function. Such apps will run fine on earlier platform versions as well as S.

core-ktxライブラリでは、拡張関数として SparseArray.set() が定義されています。このメソッドは、Android S までのAndroid SDKには存在しません。多くのアプリはこの拡張関数を使用し、S以前のSDKに対してコンパイルしています。つまり、ライブラリの関数に解決されます。そのようなアプリは、Sだけでなく、それ以前のプラットフォームのバージョンでも問題なく実行されます。

When these apps are updated to compile against S, the Kotlin compiler will resolve calls to the S SDK function which only exists on devices running the S platform; however, these apps are expected to run against earlier platform versions. They will crash when run against pre-S platforms since the method does not exist.

これらのアプリを Android S に対してコンパイルするように更新すると、Kotlin コンパイラーは、S プラットフォームを実行しているデバイスにのみ存在する S SDK 関数の呼び出しを解決しますが、これらのアプリは以前のプラットフォームのバージョンに対して実行されます。S以前のプラットフォームで実行すると、このメソッドが存在しないためクラッシュします。

As far as I can tell, there is nothing that either app developers or the core-ktx library can do about this situation. The library cannot remove the function without breaking binary compatibility with dependent libraries and regressing functionality. The app developer cannot explicitly force Kotlin to call the extension function on earlier platforms, either in their app code or dependent libraries.

私の知る限り、アプリ開発者にもcore-ktxライブラリにも、この状況に対してできることは何もないようです。ライブラリは、依存ライブラリとのバイナリ互換性を破壊し、機能を後退させることなく、この関数を削除することができません。アプリ開発者は、アプリのコードや依存ライブラリで、以前のプラットフォームで拡張関数を呼び出すように明示的にKotlinに強制することはできません。

Extension function resolution policy of "member always wins" is problematic for Android SDKs

Android SDKでは「メンバーが常に勝つ」という拡張関数解決ポリシーが問題視されている

👉 Extension function resolution policy of "member always wins" is problematic for Android SDKs : KT-45968 

SparseArray.set was added as an alias for SparseArray.put in Android S.
Consider backporting that by rewriting SparseArray.set to SparseArray.put.

SparseArray.set はSparseArray.put のエイリアスとして Android S に追加されました。
SparseArray.setをSparseArray.put に書き換えてバックポートすることを検討するべし。

👉 Backport SparseArray.set [185547135] - Visible to Public - Issue Tracker 

対処方法

put(Int, Object) の形式に戻して修正されたバージョンのSDKを待つ。


mySparseArray[3] = someObject

  ↓

mySparseArray.put(3, someObject)


AndroidStudio 利用する Java (JDK) の選択・設定の方法

AndroidStudio 上でプロジェクトテンプレートの「KMM Application」を動かそうとすると以下のようにコケる。


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`.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 509ms
Command PhaseScriptExecution failed with a nonzero exit code

** BUILD FAILED **


The following build commands failed:
PhaseScriptExecution Run\ Script
/Users/maochanz/AndroidStudioProjects/KMM/build/ios/iosApp.build/Debug-iphoneos/iosApp.build/Script-7555FFB524251A00829871.sh (in target 'iosApp' from project 'iosApp')
(1 failure)

先程のビルドエラーのメッセージにもあるように、Java 11 (JDK) を IDEである AndroidStudio に認識させる方法として、3つが挙げられている。

- IDE設定の変更
- 環境変数 JAVA_HOME の変更
- gradle.properties の org.gradle.java.home の変更

この3つのうち、どれを変更すればいいのか。

 

■ IDE設定の変更

AndroidStudio には、以下でビルド時のJDKを選択できる。


[Preferences]

  ↓

[Build]

  ↓

[Build Tools]

  ↓

[Gradle JDK]

embed jdk

現在の AndroidStudio には、「openjdk version "11.0.10" 2021-01-19」 が同梱されており、それを選択することもできるし、別に自分でインストールしているものも選択可能になっている。

しかし、IDEA製品では、自分で入れたJDKを使うほうが良さそう。


インストールしたら、パスを確認しておきましょう。


❯ /usr/libexec/java_home -V
Matching Java Virtual Machines (1):
    11.0.18 (x86_64) "Homebrew" - "OpenJDK 11.0.18" /usr/local/Cellar/openjdk@11/11.0.18/libexec/openjdk.jdk/Contents/Home
/usr/local/Cellar/openjdk@11/11.0.18/libexec/openjdk.jdk/Contents/Home

影響範囲: AndroidStudio 上のすべてのプロジェクト

 

■ 環境変数 JAVA_HOME の変更

利用するユーザーの環境変数の JAVA_HOME に設定する。

先程、確認した同梱JDKのパスをセットします。


❯ vi .zshrc
...
export JAVA_HOME=$(/usr/libexec/java_home -v11)
export PATH="$JAVA_HOME/bin:$PATH"

❯ source .zshrc

❯ echo $JAVA_HOME
/usr/local/Cellar/openjdk@11/11.0.18/libexec/openjdk.jdk/Contents/Home

❯ java -version
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment Homebrew (build 11.0.18+0)
OpenJDK 64-Bit Server VM Homebrew (build 11.0.18+0, mixed mode)


この環境変数を利用して AndroidStudio は、JDK の位置を認識します。

しかし、ユーザー単位の環境変数なので、これを利用した他のアプリケーションにも影響するかもしれません。

AndroidStudio の再起動で反映されます。

影響範囲: OS上 JAVA_HOME 利用アプリケーションたち

 

■ gradle.properties の org.gradle.java.home の変更

gradle.properties は、AndroidStudio 上のプロジェクトの gradle 設定ファイルです。

以下のように追記します。


org.gradle.java.home=/usr/local/Cellar/openjdk@11/11.0.18/libexec/openjdk.jdk/Contents/Home

AndroidStudio 上部メニューから [Build] → [Rebuild Project] で反映されます。

Build - Rebuild project

影響範囲: AndroidStudio上 該当プロジェクト

 

■ まとめ

KMM Application


❯ ./gradlew -version

------------------------------------------------------------
Gradle 7.6
------------------------------------------------------------

Build time:   2022-11-25 13:35:10 UTC
Revision:     daece9dbc5b79370cc8e4fd6fe4b2cd400e150a8

Kotlin:       1.7.10
Groovy:       3.0.13
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          11.0.18 (Homebrew 11.0.18+0)
OS:           Mac OS X 13.1 x86_64


ビルド自体は、JAVA_HOME の変更か、gradle.properties の変更、のどちらか一つで通り、生成されたアプリも問題ないようにみえる、が正しくはどうなのか。

上記の設定で改善されない場合は以下で。


👉 Mac に Homebrew で OpenJDK11 を インストール する hatena-bookmark
👉 JetBrains Toolbox で Android Studio の Stable/Beta/Canary が同時に管理できる? hatena-bookmark
👉 Android Studio と JDKバージョン 


KMM まとめ 速報 - Kotlin Multiplatform Mobile 【#ios | #android】

このページは随時更新していくページです。

まずは、2022年 Stable版リリースまで追っていきます。

開発時に使えるだろう記事をフォーカスしてまとめておきます。

連絡などは Twitter や Facebook の DM でどうぞ。

KMM まとめ 速報 ( Kotlin Multiplatform Mobile )【#ios | #android】



おすすめ

Googleは、この状況を改善するために、すべてのKotlinターゲットのための適切なマルチプラットフォームの成果物として、Composeランタイムを出荷すべきです。残念ながら、彼らのKotlinマルチプラットフォームの話は、コミュニティのニーズから数年遅れており、これがすぐに実現する見込みは非常に低い。

👉 Multiplatform Compose and Gradle module metadata abuse - Jake Wharton 

 

- New KMM library project wizard.
- Support for the new type of KMM library distribution: XCFramework.
- Enabled HMPP for new KMM projects.
- Support for explicit iOS targets declaration.
- Enabled KMM plugin wizards on non-Mac machines.
- Support for subfolders in the KMM module wizard.
- Support for Xcode Assets.xcassets file.
- Fixed the plugin classloader exception.
- Updated the CocoaPods Gradle Plugin template.
- Kotlin/Native debugger type evaluation improvements.
- Fixed iOS device launching with Xcode 13.

👉 KMM plugin releases | Kotlin 

クロスプラットフォームモバイル環境向け SDK の Kotlin Multiplatform Mobile(KMM)は、2020年8月にアルファ版として公開されました。

👉 アルファ版以降の KMM 新着情報 | The Kotlin Blog 

公式

👉 アルファ版以降の KMM 新着情報 | The Kotlin Blog 
👉 Kotlin Multiplatform Mobile がアルファ段階に移行 | The Kotlin Blog 
👉 Kotlin Multiplatform Mobile | Kotlin 

その他

👉 5分でわかる、Kotlin Multiplatform Mobile (KMM) 
👉 KMMでiOS・Android
を共通化しよう - dely tech blog 
👉 KMMというもう一つの選択肢 | bravesoft blog 
👉 Kotlin Multiplatform Mobile(KMM)がベータへ。コンカレント処理でのメモリリークフリー、iOSエコシステムとの連携強化など - Publickey 

実際に確認してみる

👉 AndroidStudio 利用する Java (JDK) の選択・設定の方法 
👉 Mac に Homebrew で OpenJDK11 を インストール する 

(このページは随時更新していくページです。)


Opt-in requirement marker annotation on override requires the same marker on base declaration

各記述を以下のように個別に置き換えるのがとりあえずシンプルでいいのかな。


@FlowPreview
→ @OptIn(FlowPreview::class)


@ExperimentalCoroutinesApi
→ @OptIn(ExperimentalCoroutinesApi::class)

gradle で一括で無視しときますか?

いうて、まだまだ変更がありそうなところではある。

Opt-in requirements (aka Experimental API support)

👉 kotlinx.coroutines/Annotations.kt at master · Kotlin/kotlinx.coroutines 
👉 [BUG] [multiplatform] ExperimentalAPI annotation from Kotlin 1.6.0 and above - githubmemory 
👉 Opt-in requirements (aka Experimental API support) · Issue #95 · Kotlin/KEEP 
👉 Opt-in requirements | Kotlin 

👉 @SuppressWarnings て使っていますか。 

2022-05-14 : 追記

→ Deprecated に。
👉 '-Xopt-in' is deprecated and will be removed in a future release, please use -opt-in instead hatena-bookmark


【Flow】 shareIn() と stateIn()

意訳。

Flow.shareIn()

Flow.shareIn
コールドフローを、指定されたコルーチンスコープで開始されるホットな SharedFlow に変換し、上流側フローの単一の実行インスタンスからの emit を複数の下流側サブスクライバと共有し、指定された数の replay 値を新しいサブスクライバに再生します。SharedFlow の一般的な概念についてはドキュメントを参照してください。

共有コルーチンの開始は、started パラメータで制御され、以下のオプションがサポートされています。

- Eagerly
最初のサブスクライバが現れる前から上流のフローが開始されます。この場合、replay パラメータで指定された最新の値を超えて上流から emit された全ての値は直ちに破棄されますのでご注意下さい。

- Lazily
最初のサブスクライバが現れた後に上流のフローを開始します。この場合、最初のサブスクライバが emit されたすべての値を取得することが保証されますが、後続のサブスクライバは最新の replay 値のみを取得することが保証されます。すべてのサブスクライバがいなくなっても、上流のフローは継続してアクティブですが、サブスクライバがいない場合は、最新の replay 値のみがキャッシュされます。

- WhileSubscribed()
最初のサブスクライバが現れたときに上流のフローを開始し、最後のサブスクライバが消えたときに即座に停止し、リプレイ・キャッシュを永遠に維持します。WhileSubscribed() には、ドキュメントで説明されているように、追加のオプション設定パラメータがあります。

- SharingStarted
インターフェースを実装することで、カスタムストラテジーを提供することができます。

shareIn オペレータは、作成や維持にコストがかかるコールドフローがあり、その値を収集する複数のサブスクライバがある場合に便利です。

例えば、バックエンドから高コストなネットワーク接続を介してメッセージが送られてきて、その確立に多くの時間を要するフローを考えてみましょう。

概念的には次のように実装されます。


val backendMessages: Flow<Message> = flow {
  connectToBackend() // takes a lot of time
  try {
    while (true) {
      emit(receiveMessageFromBackend())
    }
  } finally {
    disconnectFromBackend()
  }
}

このフローをアプリケーションで直接使用する場合は、収集するたびに新しい接続が確立されるため、メッセージが流れ始めるまでに時間がかかります。しかし、次のように1つのコネクションを共有して、それを確立することができます。


val messages: SharedFlow<Message> = backendMessages.shareIn(scope, SharingStarted.Eagerly)

これで messages から1つのコネクションがすべてのコレクター間で共有され,必要なときにはコネクションを確立しておくことができます。

* 上流の完了とエラー処理

上流側フローの通常の完了は、サブスクライバには影響を与えず、共有コルーチンは継続して実行されます。SharingStarted.WhileSubscribed が使用されている場合は、上流側が再び再開されます。それの完了時に特別なアクションが必要な場合は、shareIn オペレータの前に onCompletion オペレータを使用して、以下のように特別な値を emit することができます。


backendMessages
  .onCompletion { cause -> if (cause == null) emit(UpstreamHasCompletedMessage) }
  .shareIn(scope, SharingStarted.Eagerly)

上流のフローで例外が発生した場合、どのサブスクライバにも影響を与えることなく共有コルーチンが終了し、それが起動したスコープで処理されます。shareIn オペレータの前に catch や retry オペレータを使用することで、カスタムの例外処理を設定することができます。例えば,IOException が発生したときに1秒の遅延で接続を再試行するには,次のようにします.


val messages = backendMessages
  .retry { e ->
    val shallRetry = e is IOException // 他の例外はバグ
    if (shallRetry) delay(1000)
    shallRetry
  }
  .shareIn(scope, SharingStarted.Eagerly)

* 初期値

上流がまだデータをロード中であることをサブスクライバに知らせるために、特別な初期値が必要な場合は、上流のフローに onStart オペレータを使用します。以下のようになります。


backendMessages
  .onStart { emit(UpstreamIsStartingMessage) }
  .shareIn(scope, SharingStarted.Eagerly, 1) // 最新のメッセージを1つ再生する

* buffer と conflate

shareIn オペレータは、別のコルーチンで上流のフローを実行し、buffer オペレータの説明にあるように、replay サイズまたはデフォルト (大きい方) のバッファを使用して、上流からの emit をバッファリングします。このデフォルトのバッファリングは,shareIn コールの前に buffer または conflate を付けることで,明示的なバッファ設定で上書きすることができます。

- buffer(0).shareIn(scope, started, 0) は、デフォルトのバッファサイズを上書きし、バッファのない SharedFlow を作成します。実際には、上流のエミッタとサブスクライバの間で順次処理が行われ、すべてのサブスクライバが値を処理するまでエミッタが停止するように設定されます。なお、サブスクライバがいない場合でも、値は直ちに破棄されます。

- buffer(b).shareIn(scope, started, r), replay = r, extraBufferCapacity = b の SharedFlow を作成します。

- conflate().shareIn(scope, started, r) が作成されます。

Flow.stateIn()

Flow.stateIn
コールドフローを、与えられたコルーチンスコープで開始される ホットな StateFlow に変換し、上流のフローの単一の実行インスタンスから最も新しく emit された値を複数の下流のサブスクライバと共有します。StateFlow の一般的な概念についてはドキュメントを参照してください。

共有コルーチンの開始は、shareIn オペレータのドキュメントで説明されているように、started パラメータによって制御されます。

stateIn オペレータは、ある状態の値の更新を提供するコールドフローがあり、作成や維持にコストがかかるが、最新の状態の値を収集する必要がある複数のサブスクライバがいる場合に便利です。例えば、バックエンドから高価なネットワーク接続を介して状態の更新が行われ、その確立に多くの時間がかかるフローを考えてみましょう。概念的には次のように実装されます。


val backendState: Flow<State> = flow {
  connectToBackend() // takes a lot of time
  try {
    while (true) {
      emit(receiveStateUpdateFromBackend())
    }
  } finally {
    disconnectFromBackend()
  }
}

このフローをアプリケーションで直接使用する場合、フローが収集されるたびに新しい接続が確立されるため、状態の更新が流れ始めるまでにしばらく時間がかかります。しかし、次のように1つのコネクションを共有し、それを熱心に確立することができます。


val state: StateFlow<State> = backendMessages.stateIn(scope, SharingStarted.Eagerly, State.LOADING)

これで、state から全てのコレクターの間で1つのコネクションが共有され、必要になった時には既にコネクションが確立されている可能性があります。

* 上流の完了とエラー処理

上流フローの正常な完了は、サブスクライバには影響を与えず、共有コルーチンは継続して実行されます。SharingStarted.WhileSubscribed が使用されている場合は、上流側が再び再開されます。その完了時に特別なアクションが必要な場合は、stateIn オペレータの前に onCompletion オペレータを使用して値を出力することができます。shareIn オペレータのドキュメントを参照してください。

上流のフローで例外が発生した場合、どのサブスクライバにも影響を与えることなく共有コルーチンが終了し、共有コルーチンが起動したスコープで処理されます。カスタム例外処理は、shareIn オペレータと同様に、stateIn の前に catch や retry オペレータを使用して設定できます。

👉 【MVVM】 Kotlin Flow で使える5つの利用パターン 
👉 【Kotlin】Flow の挙動やライフサイクルをログで確認する hatena-bookmark