AndroidのViewModelでリソース使いにくい問題

3 min read読了の目安(約2800字

概要

Androidの ViewModel でリソース使いにくい問題をどうにかしよう

背景

Androidの ViewModel を実装していて、リソースを使いたいんだけど Context がないんだよな〜🥺 となったことが一度くらいあるんじゃないかと思います。
そんな時、例えばDIコンテナを使っていたら ContextResources をラップした型をinjectして使えば簡単に解決できるようになります。しかし、DIコンテナを導入していないプロジェクトではそうもいきません。そうした場合でも、ちょっとしたコードを記述することでDataBindingを利用して Context を使わずにリソースを ViewModel から扱うことができるようになります。

詳細

というわけで、以下詳細ですが、詳細に移る前にざっとした流れを先に説明します。

  1. DataBindingを使うと Context が使えるよ
  2. DataBindingのタイミングまでリソースの解決を遅延させれば解決しそうだね
  3. リソースの情報を保持するデータ構造を用意しよう

という感じですね。

DataBinding

みんな大好きDataBindingです。
DataBindingを使うとなんと Context を得ることができます。
具体的には BindingAdapter を記述してあげるということになります。
BindingAdapter を記述すると ViewTextView を引数にとることができますから、これらの 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 ではあまり ContextResources に触りたくないですよね。

リソースの情報を保持するデータ構造

そこで、リソースの情報を保持しつつ、 BindingAdapter で後からリソースを解決するためのデータ構造を用意してあげることで、 ViewModel では ContextResources を触らなくてよくすることができるようになります。

文字列を扱う場合はこんな感じですね。

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していただけたらと思います。

https://github.com/crimsonwoods/EasyDataBinding