hiro99ma blog

何か技術的なこと

android: compose で system service はどこでもらうか

2024/10/22

前回で ViewModel を使うところまでチュートリアルを動かしたので、また BLE 操作のアプリ作成に戻る。
じっくりやっても身につく気がしないので、今は作ってみるときだ、と判断した。
まあ、そうじゃなかったら調べれば良いだけだ。

BluetoothLeScanner

Android での BLE機器のスキャンは OS バージョンで変わってくるのでめんどくさい。

最終的には BluetoothLeScanner を使う。
そのために BluetoothLeScanner のインスタンスがいるのだが、説明に書いてあるように BluetoothAdapter#getBluetoothLeScanner() を使う。
そして BluetoothAdapter もインスタンスがいるのだが、これも説明に書いてあるように BluetoothManager.getAdapter() を使う。
BluetoothManager もインスタンスが必要で、これは Context.getSystemService() で取得する。

Context.getSystemService() は Android のサービス系の機能を使いたいときにはだいたい出てくる。
今回は Bluetooth なので Kotlin だと getSystemService(BluetoothManager::class.java) のような呼び出し方になる。
そしてこの getSystemService() をどこで呼び出すのが良いんだろうか、というのが今回の話題である。

ちなみに Kotlin だからか .getAdapter() みたいなメソッドは .adapter とプロパティになっていることが多い。

Context

Activity

Context の大元は Activity だろう。
アプリを Android Studio のウィザードで作った場合は MainActivity という名前になっている。
その中のメソッドであれば getSystemService() だけで呼び出すことができる。

ただ Compose で画面を作っているので Activity にお願いするのもなんだかなぁ、と思ってしまう。

Composable

@Composable関数の中では LocalContext.current で現在の Context を取得できるようだ。
なので LocalContext.current.getSystemService() で呼び出せる。

まあ、これはこれで仕方ないとあきらめても良い気もする。
ただ BLE にアクセスするのはさらに下の階層なので、ViewModel を使うにしてもそこに与えないといけない。
コンストラクタで与えるべきか、ViewModel に設定するメソッドを追加するか悩ましい。

ViewModel

Android のアプリアーキテクチャに沿うならば BLE のアクセスは ViewModel の仕事ではないだろう。
Data Layerがネットワークやデータベースにアクセスしてデータを取得するなら、BLE の機器一覧を取得する機能も Data Layer だと思う。
まあ、そんなに堅苦しく考えなくてよいとは思うが今回はアプリアーキテクチャの勉強でもあるので。

なので、BluetoothLeScanner を Composable –> ViewModel –> Repository –> Data Source、と伝えないといけない。
データベースやインターネットが追加されたら、それらも同じようなことになるだろう。
そう思うと下の方には Context を渡して getSystemService() を実行するのは下の方に任せてもいいんじゃないの、と思ったのだ。

サンプルを見る

アプリアーキテクチャのサンプルがあるので、これを見る。

たぶん Data Layer か Data Source のどちらかだろう。
MyModelRepository.kt はいるので Data Source かな?
ただこのクラスは定義はあるもののテンプレートでは使われていない。

github.com/android を探してようやく getSystemService() を下の方で使っている例が見つかった。

このコードも呼ばれている感じがしない。
context に付いている @ApplicationContext@Inject は Hilt という依存関係インジェクションなるものに関係するらしい。

internal class ConnectivityManagerNetworkMonitor @Inject constructor(
    @ApplicationContext private val context: Context,
    @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
) : NetworkMonitor {
    override val isOnline: Flow<Boolean> = callbackFlow {
        trace("NetworkMonitor.callbackFlow") {
            val connectivityManager = context.getSystemService<ConnectivityManager>()
            ...

image

DI というらしいが、なんだこりゃ?

Hilt

Dependency Injection のことらしい。
delegation のように引数で与えるのは「手動」というやり方だそうな。
それに対してライブラリを使うやり方があり、HiltDagger という DIライブラリを使った Android ライブラリ。

Compose じゃないので私にはちょっとつらい。
雰囲気は分かる気がするのだが、たとえば Context を注入するのに @ActivityContext をコンストラクタの引数に書いていた場合、@AndroidEntryPointActivity からうまいこと Context を引っ張ってきてあたかも引数で与えたかのように振る舞ってくれるというととだろうか。

image

@ActivityContextがよいのか@ApplicationContextがよいのかはぱっと判断できない。
BLE はデバイスに紐付くからどっちでもよいと思うと @ApplicationContext の方が意味がわかりやすいかしら。

が、そもそも Hilt がよくわからないので試したい。
@ApplicationContextは本当に思ったように動作するのかとか、@ApplicationContextじゃないパラメータでそっちは Hilt で注入するようにしてなかったらどうなるの、とか。
BLE でいきなり試すのは挙動が分からなさすぎるので、より手前の ViewModel が確認しやすいだろう。
幸い ViewModel についても @HiltViewModel という annotation が用意されている ので

@HiltViewModel@Inject(javax.inject.Injectなんだね)を付けてビルド。 ライブラリや@AndroidEntryPointなどは付けているのだがエラーになる。

[Dagger/MissingBinding] android.content.Context cannot be provided without an @Provides-annotated method.

自作 ViewModel のコンストラクタ引数が Context なのだがよろしくないのか。 引数がIntでもダメで、引数無しだとビルドできる。
どうもパラメータが付くと対応がいるらしい。

hiltViewModel とか viewModels とか小文字始まりのが出てくるのだけど、私のところで解決できない。。。 どうも “androidx.hilt:hilt-navigation-compose” を implementation しないと使えないようだ。

これで Gradle Sync はできるようになったのだが、どうにもビルドエラーになる。
“kspDebugKotlin” だの “already registered” だの “internal error” だの別の要因な気がする。

2番目の回答のように gradle.propertiesksp.incremental=false を追加したら直った。
PC を再起動とかでももしかしたら直ったパターンかもしれない。
よくわからん。。。

まだ ViewModel のコンストラクタを Int にしたままだったので Context に戻す。
そうするとコンパイルエラーが。。。

image

今日はおしまい。

おまけ

テンプレートのカスタマイズ

このテンプレートのカスタマイズスクリプト ./customizer.sh your.package.name DataItemType [MyApplication] だが、MyApplication はファイル名やクラス名として使われるようなので変な名前にしないよう気をつけよう!

あと、AGP のバージョン androidGradlePlugin が現在の最新になるようで Android Studio の安定版だとビルドできないかもしれない。
これを書いている時点で私は “Koala Feature Drop | 2024.1.2” を使っていて AGP は 8.6.0 だがテンプレートは 8.7.0 だった。

< Top page