ViewModelにおける文字リソースの扱いと改めて向き合ってみる中で、幾つかネット上で選択肢が見つかったものの、どれもしっくりこなかったので自分なりに解決策を考えてみました。
見つけた方法たち
AndroidViewModelを使う
class MainViewModel(application: Application) : AndroidViewModel(application) {
val text: LiveData<String> = Transformations.map(this.name) { name ->
this.getApplication<Application>().getString(R.string.hello, name)
}
}
今まではこの方法でやっていました。
しかしこの方法だと、application contextなので、例えばユーザーが言語設定を変更したとき、再起動するまで反映されません[1]。
そういうものだと思えばそれでも良いかもしれませんが、できればactivity contextを適用してあげたい。
StringProvider
class StringProvider(val context: Context) {
fun getString(resId: Int, vararg formatArgs: Any) = this.context.getString(resId, formatArgs)
}
class MainViewModel(stringProvider: StringProvider) : ViewModel() {
val text: LiveData<String> = Transformations.map(this.name) { name ->
this.getApplication<Application>().getString(R.string.hello, name)
}
}
この方法を採用している人も見かけました[2]。
ViewModelの直接的なcontext依存がなくなり、テストの書きやすさは向上しそうですが、StringProviderに対してactivity contextで依存解決するのは少々手間がかかります。Application contextを適用すれば、「AndroidViewModelを使う」と同様の問題が生じます。
XMLで変換する
android:text="@{@string/hello(viewModel.name)}"
ViewModelではgetStringの変数(ここではname
)だけを持たせておき、XML側で文字リソース指定するやり方もあります[3]。
これなら、「AndroidViewModelを使う」のapplication context問題は回避できます。
ただ、条件によって文字リソースを切り替えたい場合、この方法では分岐をViewModel内で完結させられません。
提案
「StringProvider」の流れを反転し、「XMLで変換する」を掛け合わせるイメージです。
internal interface StringResource {
fun apply(context: Context): String
companion object {
fun from(@StringRes resId: Int, vararg formatArgs: Any) = object : StringResource {
override fun apply(context: Context) = context.getString(resId, formatArgs)
}
}
}
class MainViewModel : ViewModel() {
val text: LiveData<StringResource> = Transformations.map(this.name) { name ->
StringResource.from(R.string.hello, name)
}
}
android:text="@{viewModel.text.apply(context)}"
StringResource生成時にresIdと変数を渡しておき、xmlでcontextを適用しています。
改善余地はありそうですが、雰囲気良さげなんじゃないかと。