android: compose で system service はどこでもらうか (2)
あらすじ
BLE スキャンするために getSystemService()をどこで呼ぶと良いか悩む私。
呼ぶにしても Context がいるため、その面倒さが何にしてもネックになることに気付く。
検討していると DI(依存性注入)ライブラリを使うとパラメータによる直接の引渡しをしないで済みそうな気配を感じる。
前回の最後は Hilt という Android 向けの DIライブラリの使い方を調べていた。
@Composable invocations can only happen from the context of a @Composable function
パラメータを Int から Context にするとエラーになった。

“invocation” は「祈り」とか「実施」のような意味。call とは違うのか。
私は「実行する」(execute)か「呼び出す」(call)しか思いつかんな。
- 英語「invocation」の意味・使い方・読み方 - Weblio英和辞書
- c# - What’s the difference between “call” and “invoke”? - Stack Overflow
- Invoke vs Call a Function – Learn the Difference - CodeSweetly
そういうのはともかく、コンテキストが違うと呼び出せないそうだ。
ここのラムダ式はコールバックみたいなものだと思っていたがコンテキストが違うのか。。。
私の場合もこちらと同じように composable関数の方でLocalContext.currentを保持してラムダ式の中で渡すことでエラーが出なくなった。
下の方ではこのContextを保持するわけではなく BluetoothManagerのインスタンスを取得するのに使う。
BluetoothManager –> BluetoothAdapter –> BluetoothLeScanner という流れだ。
下の方で保持するのだが Activity の作り直しが発生すると Context も変更になるのだろうか?
デバッガで確認したところ、composable 関数での Localcontext.current は MainActivity だった。
(ActivityもApplicationもandroid.appパッケージ。)
ではApplicationContextはどこから取得したら良いだろうか。
しばし悩んだが、Activity.getApplicationContext() で取得できるので同じように LocalContext.current.applicationContext でよい。
DI の便利さ
昨日 Hilt で DI した(という使い方で良いのか?)のは Context だった。
が、これは単に ScanViewModel のコンストラクタ引数を Context と書いて、呼び出す ScanScreen からも Activity の context を引数に書いただけで、全然 Hilt を導入したよさがない。
そもそもどうなるのが便利なんだっけ?
- HiltViewModelで始めるAndroidの依存性注入のお話 #JetpackCompose - Qiita
- DIライブラリ「Hilt」のセットアップ&使い方(Kotlin) #Android - Qiita
1つしか ViewModel がないとありがたみがわからないようだ。
複数 ViewModel があって、それぞれがまた Repository やらなんやらを使うようなところを参照するようなところが便利になるらしい。
それ以外にも、よく使うようなパラメータについては組み込み済みのアノテーションがあって楽して使えるようだ。
そう、今回は Context を上から直接もらうように書かなくても何とかなるんじゃないの、というのが目的だったのだ。
Context をどこかからもらう
というわけで、苦労した Hilt で ViewModel をパラメータ付きにする箇所を、パラメータをもらわないで済むように変更する。
(“ApplicationContext” というユーザがいたので、アノテーション @ApplicationContext を git コメントに書いたらその人へのリンクになったじゃないか。。。)
ViewModel の定義はこうなった。
@HiltViewModelだけ@InjectContextのパラメータに@ApplicationContextを付けるScanViewModelFactoryは削除- もらった
ContextでBluetoothLeScannerのインスタンスをもらってスキャンできるようにしておく
@HiltViewModel
class ScanViewModel @Inject constructor(
@ApplicationContext context: Context
): ViewModel() {
呼び出したのは ScanScreen 側。
contextを引数にせず、単にhiltViewModel()でインスタンスを作る(型はScanViewModel)
これだけである。
ブレークポイントで止めると context は MainApplication になっていたので指示通りだ。
ちゃんとスキャンも動作したので Context に問題はないだろう。
ScanViewModel のコンストラクタ引数に 1つ Int を追加したところ、前の assisted injection に対応せずパラメータを追加したときと同じエラーが出た。
Context は全然出てこなかった。
そういうものなんだ。
DI再履修
なんか、これくらいだったらコンストラクタに Context を渡しても大して差がないような気がしてきた。
パラメータが増えてきたら面倒かもしれないが、たぶんそういう目的じゃないな。
- Android での依存関係インジェクション - Android Developers
- メリット
- コードを再利用できる
- リファクタリングが容易になる
- テストが容易になる
- メリット
DI の話と、DI ライブラリの話を混ぜ込んで考えてしまったようだ。
コンストラクタで「これ使ってね」と依存しているインスタンスを投げ込むのも DI なのだった。
あとは、そういうしくみが手動なのか自動なのかという話なのだな。
もうちょっと学習が必要なので、今日はここまで。
外部からインスタンスを受け入れるようにすれば DI というわけでもなさそうだ。