AndroidのViewModelでリソース使いにくい問題
概要
Androidの ViewModel
でリソース使いにくい問題をどうにかしよう
背景
Androidの ViewModel
を実装していて、リソースを使いたいんだけど Context
がないんだよな〜🥺 となったことが一度くらいあるんじゃないかと思います。
そんな時、例えばDIコンテナを使っていたら Context
や Resources
をラップした型をinjectして使えば簡単に解決できるようになります。しかし、DIコンテナを導入していないプロジェクトではそうもいきません。そうした場合でも、ちょっとしたコードを記述することでDataBindingを利用して Context
を使わずにリソースを ViewModel
から扱うことができるようになります。
詳細
というわけで、以下詳細ですが、詳細に移る前にざっとした流れを先に説明します。
- DataBindingを使うと
Context
が使えるよ - DataBindingのタイミングまでリソースの解決を遅延させれば解決しそうだね
- リソースの情報を保持するデータ構造を用意しよう
という感じですね。
DataBinding
みんな大好きDataBindingです。
DataBindingを使うとなんと Context
を得ることができます。
具体的には BindingAdapter
を記述してあげるということになります。
BindingAdapter
を記述すると View
や TextView
を引数にとることができますから、これらの View
から Context
を取り出すことができるようになります。
@BindingAdapter("okCancel")
fun TextView.setOkCancelText(value: Boolean) {
text = context.getString(if (value) {
android.R.string.ok
} else {
android.R.string.cancel
})
}
こんな感じで、 BindingAdapter
を記述してあげるとその中では Context
が使えます。
リソースの解決を遅延させる
上の項では、 BindingAdapter
を用意することでDataBindingを使えば Context
の解決は後からできることを示しました。もし、リソースの解決を ViewModel
のロジック内からDataBindingでのbinding実行時まで遅延することができたなら、「 ViewModel
では必要なリソースの選択だけを行い、リソースの解決はbindingのタイミングで行う」ということができるようになりますね。
コードで書くとこんな感じです。
class MyViewModel: ViewModel() {
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
val onClickButton: View.OnClickListener
get() = View.OnClickListener { view ->
// リソースの解決を後回しにできたら、ここでContextやResourcesを触らなくてよくなる
_message.value = if (view.isActive) {
view.context.getString(R.string.xxx)
} else {
view.context.getString(R.string.yyy)
}
}
}
ViewModel
ではあまり Context
や Resources
に触りたくないですよね。
リソースの情報を保持するデータ構造
そこで、リソースの情報を保持しつつ、 BindingAdapter
で後からリソースを解決するためのデータ構造を用意してあげることで、 ViewModel
では Context
や Resources
を触らなくてよくすることができるようになります。
文字列を扱う場合はこんな感じですね。
data class Text(@StringRes resId: Int)
@BindingAdapter("android:text")
fun TextView.setText(text: Text) {
this.text = context.getString(Text.resId)
}
こんな感じで Text
型を用意してあげて、それを ViewModel
から使います。
class MyViewModel: ViewModel() {
private val _message = MutableLiveData<Text>()
val message: LiveData<Text> = _message
val onClickButton: View.OnClickListener
get() = View.OnClickListener {
_message.value = if (view.isActive) {
Text(R.string.xxx)
} else {
Text(R.string.yyy)
}
}
}
こんな感じで Context
が必要なくなりました。
いかがでしたか?
ちょっとした工夫で ViewModel
が作りやすくなったんじゃないかなと思います。
宣伝
ということで最後に宣伝です。
上記のようなことをできるようにするためのライブラリを作っています。
jitpackでプレリリース版を配布しているので試しに使ってみたい方がいらっしゃったらどうぞご利用ください。
また、機能的にも今後もう少し拡充していこうかなと思っています。
もし興味があったらstarやwatchしていただけたらと思います。
Discussion