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>()
...
DI というらしいが、なんだこりゃ?
Hilt
Dependency Injection のことらしい。
delegation のように引数で与えるのは「手動」というやり方だそうな。
それに対してライブラリを使うやり方があり、Hilt は Dagger
という DIライブラリを使った Android ライブラリ。
Compose じゃないので私にはちょっとつらい。
雰囲気は分かる気がするのだが、たとえば Context
を注入するのに @ActivityContext
をコンストラクタの引数に書いていた場合、@AndroidEntryPoint
な Activity
からうまいこと Context
を引っ張ってきてあたかも引数で与えたかのように振る舞ってくれるというととだろうか。
@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
でもダメで、引数無しだとビルドできる。
どうもパラメータが付くと対応がいるらしい。
- 中途半端にDIしたい ▶︎HiltのAssisted Injectを使う
- Jetpack Compose + HiltでViewModel生成時にパラメータを渡す #Android - Qiita
- View Models
hiltViewModel
とか viewModels
とか小文字始まりのが出てくるのだけど、私のところで解決できない。。。
どうも “androidx.hilt:hilt-navigation-compose” を implementation しないと使えないようだ。
これで Gradle Sync はできるようになったのだが、どうにもビルドエラーになる。
“kspDebugKotlin” だの “already registered” だの “internal error” だの別の要因な気がする。
2番目の回答のように gradle.properties
に ksp.incremental=false
を追加したら直った。
PC を再起動とかでももしかしたら直ったパターンかもしれない。
よくわからん。。。
まだ ViewModel のコンストラクタを Int
にしたままだったので Context
に戻す。
そうするとコンパイルエラーが。。。
今日はおしまい。
おまけ
テンプレートのカスタマイズ
このテンプレートのカスタマイズスクリプト ./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 だった。