android: Hilt対応
2024/10/25
ViewModel を継承したクラスに @HiltViewModel
を付けて実装しておくと、Activity の方では hiltViewModel()
でうまいこと ViewModel を生成してくれる。
では、BLE機器をスキャンする BleScan
クラスを ViewModel で生成するならどうするとよいか。
ただし BleScan
はコンストラクタで ApplicationContext
をパラメータとして受け取るものとする。
見よう見まねだ。
- @Binds を使用してインターフェース インスタンスを注入する
@Binds
と@Provides
が載っていたが、@Provides
は自分で制御できないモジュールの例になっていたので@Binds
にした- まず、
BleScan
クラスをDefaultBleScan
という名前にしておく- 名前はなんでもよい。ここではサフィックスに
Impl
を付けているが、archtecture-templates だとプレフィクスにDefault
と付けていたのでそちらにあわせた。
- 名前はなんでもよい。ここではサフィックスに
BleScan
インターフェースを作り、DefaultBleScan
に継承させる@Module
と@InstallIn()
を付けたabstract class BleScanModule
を作る@Binds
の付いたabstract fun bindBleScan(DefaultBleScan): BleScan
を作る
あとは ViewModel の方で Context
を取る必要がなくなったので削除したくらいか。
これだけの記述で、動く。
不思議だ。。。
Android Studio では↑で作ったクラスなどが未使用っぽく見えてしまうが、直接 Hilt/Dagger が参照しているわけではないので仕方ない。
ぜいたくを言えば、BleScan
のファイルの中には BLE操作だけのコードだけにして、Hilt に関係するのは別のファイルにしたいという気持ちがある。
そういう場合は、自分で所有していないクラスについても対応できる @Provides
を使うとよさそうだ。
@Binds
がコンストラクタを紐付けるのに対して @Provides
は生成する処理を紐付けるというところか。
@ApplicationContext
などもこちらの書けるので、本体側は普通の実装だけ書けば良い。
コンストラクタを直接ではなく、一度生成するメソッドを呼び出すので @Provides
の方がちょっとだけ効率が下がるのかな?
下がるという程でもない気はする。そんなにバンバン生成するわけでもないだろうし。
archtecture-templates の di.DatabaseModule もそうやっていた。
こちらは複数メソッドがあるので、一つ上の local.database
パッケージにあるクラスを 1箇所で対応するという書き方もできるのだろう。
おまけ
nullable な関数型の呼び出し
BLE機器をスキャンするとコールバックで機器情報が返ってくる。
スキャン中は見つかり次第返ってくる(同じ機器が返ってくるのは RSSI が変わったからかもしれんが)ので、今回の BleScan
でもスキャンした結果は上側に返そうと思う。
そのとき、スキャン開始でコールバック関数を付けて呼び出してもらうとする。
コールバック関数を引数に取る関数を higher-order function と呼ぶ。
関数型を戻す関数のことも higher-order function と呼ぶようだ。
OS が結果をコールバックするのはスキャン開始した関数の中ではなかったので、付けてもらったコールバック関数は BleScan
で保存しておく。
private var resultCallback: ((String) -> Unit)? = null
これを実行しようと素直に書くと、これはダメだった。
if (resultCallback != null) {
resultCallback(result)
}
関数型の変数を呼び出すのにはinvokeを使うものらしい。
が、単にf(x)
でもよいとも書いてある。
invoke
は関数ではなく演算子(operator function)らしい(11.1.3 Callables and invoke convention)。
いろいろ書いてあるが深追いしない。。。
resultCallback?.invoke(result)
Android Studio では修正候補がもう1つ出てきて、こちらが優先?だった。
Execute if not null というものらしい。
resultCallback?.let { it(result) }
どちらも if
での null チェックは不要なようだ。
でも、if
文で書いても大したことないと思う。
わざわざ指摘するということはなんか理由があるのだろう。
コンテキストが違うので if
文でチェックしたときは null じゃなかったけど呼び出そうとしたら null になる可能性があるというところか。