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バージョン 


ContentProvider を Flow 化する方法 - CashApp Cooper

cashapp/cooper


fun ContentResolver.observeQuery(
  uri: Uri,
  projection: Array<String>? = null,
  selection: String? = null,
  selectionArgs: Array<String>? = null,
  sortOrder: String? = null,
  notifyForDescendants: Boolean = false
): Flow<Query> {
  val query = ContentResolverQuery(this, uri, projection, selection, selectionArgs, sortOrder)
  return flow {
    emit(query)


    val channel = Channel<Unit>(CONFLATED)
    val observer = object : ContentObserver(mainThread) {
      override fun onChange(selfChange: Boolean) {
        channel.offer(Unit)
      }
    }


    registerContentObserver(uri, notifyForDescendants, observer)
    try {
      for (item in channel) {
        emit(query)
      }
    } finally {
      unregisterContentObserver(observer)
    }
  }
}

👉 FlowContentResolver.kt#L43-L90
👉 copper/FlowContentResolver.kt at trunk · cashapp/copper

Kotlin coroutines Flow や RxJava Observable を使ったリアクティブクエリ用の ContentProvider のラッパーです。

使用方法


implementation 'app.cash.copper:copper-flow:1.0.0'

ContentResolver で query() を observeQuery() に変更することで、リアクティブ版を実現します。


contentResolver.observeQuery(uri).collect { query ->
  query.run()?.use { cursor ->
    // ...
  }
}

query() とは異なり、observeQuery() は Query オブジェクトを返します。このオブジェクトは、カーソルの基礎となるクエリを実行するために run() を呼び出す必要があります。これにより、値をキャッシュする中間オペレータがリソースをリークすることなく、コンシューマーがカーソルのライフタイム全体にアクセスできるようになります。

cursor を直接処理する代わりに、含まれる値をセマンティックタイプに変換するためのオペレータを提供しています。


contentResolver.observeQuery(uri)
  .mapToOne { cursor ->
    Employee(cursor.getString(0), cursor.getString(1))
  }
  .collect {
    println(it)
  }


Employee(id=bob, name=Bob Bobberson)

mapToOne オペレータは、1 つの行を返すクエリを受け取り、ラムダを起動してカーソルを希望の型にマッピングします。クエリがゼロまたは1行を返す場合は、コルーチン成果物には mapToOneOrNull オペレータがあり、RxJava成果物には mapToOptional 演算子があります。

クエリがリストを返す場合は、同じラムダでmapToListを呼び出します。


contentResolver.observeQuery(uri)
  .mapToList { cursor ->
    Employee(cursor.getString(0), cursor.getString(1))
  }
  .collect {
    println(it)
  }


[Employee(id=alice, name=Alice Alison), Employee(id=bob, name=Bob Bobberson)]

安定の神Jake産です。

👉 【SQLDelight 】Query を Flow 化するプラグイン