Androidで開発する際、文字列のリソースは res/values/strings.xml
に書いて管理すると思います。
それらを実際に文字列として取得するためには、contextが必要になります。
val text = context.getString(R.string.hoge)
一方で、ViewModelにActivity contextを渡すことはメモリリークの危険性があるため、アンチパターンとされています。
Application contextで文字列を取得する方法もあると思いますが、言語切替時に正しくUIが更新されない等、あまり良くないと感じました。
class HogeViewModel(
private val applicationContext: Context
): ViewModel() {
val text: LiveData<String> = MutableLiveData(applicationContext.getString(R.string.text))
}
ViewModelではenum等を作成し、View側で文字列に変換する等の方法もあると思いますが、細かい文字列の制御が難しくなります。
enum class Status {
Status1, Status2, Status3
}
class HogeViewModel: ViewModel() {
val text: LiveData<Status> = MutableLiveData(Status.Status1)
}
class HogeActivity: ViewModel() {
private val viewModel: HogeViewModel by viewModels()
override fun onCreate() {
viewModel.text.observe(this) {
val resId = when(it) {
Status.Status1 -> R.string.status1
Status.Status2 -> R.string.status2
Status.Status3 -> R.string.status3
}
val text = context.getString(resId)
}
}
}
今回はActivity contextを使いつつ、いい感じにString resourcesを扱う方法について紹介したいと思います。
以下のようなStringResourceクラスを作成します。
data class StringResource(
@StringRes private val resId: Int,
private val params: List<Any> = emptyList()
) {
companion object {
fun create(
@StringRes resId: Int,
vararg params: Any
): ResStringResource {
return ResStringResource(resId, listOf(*params))
}
}
override fun getString(context: Context): String {
if (params.isEmpty()) {
return context.getString(resId)
}
return context.getString(resId, *params.toTypedArray())
}
}
ViewModelではStringResourceを返すようにします。
class HogeViewModel : ViewModel() {
val text1: LiveData<StringResource> = MutableLiveData(StringResource.create(R.string.hello_world))
// パラメータ指定もできます
val text2: LiveData<StringResource> = MutableLiveData(StringResource.create(R.string.hello_world, param))
}
あとは、View側でgetStringをすればOKです。
class HogeActivity: AppCompatActivity() {
private val viewModel: HogeViewModel by viewModels()
override fun onCreate() {
viewModel.text1.observe(this) {
val text1 = it.getString(this)
}
viewModel.text2.observe(this) {
val text2 = it.getString(this)
}
}
}
DataBindingを使ってる場合、xmlから直接呼び出すこともできます
android:text="@{viewModel.text1.getString(context)}"
android:text="@{viewModel.text2.getString(context)}"
シンプルなため、使い方にはそこまで困らないと思います。
また、data classにしていることで、testも書きやすくなっています。
上記のクラスで、ほとんどのケースは担保できると思いますが、下記のように、複数のリソースを組み合わせたり、フォーマットの中にさらにリソースを渡したいみたいなケースも稀にあると思います。
val a = context.getString(R.string.a)
val b = context.getString(R.string.b)
val text1 = "$a:$b"
val param = context.getString(R.string.param)
val text2 = context.getString(R.string.text2, param)
それらの実現のために、StringResourceを抽象化し、以下のように拡張します。
interface StringResource {
fun getString(context: Context): String
}
data class ResStringResource(
@StringRes private val resId: Int,
private val params: List<Any> = emptyList()
) : StringResource {
companion object {
fun create(
@StringRes resId: Int,
vararg params: Any
): ResStringResource {
return ResStringResource(resId, listOf(*params))
}
}
override fun getString(context: Context): String {
if (params.isEmpty()) {
return context.getString(resId)
}
val params = params.map {
if (it is StringResource) {
it.getString(context)
} else {
it
}
}
return context.getString(resId, *params.toTypedArray())
}
data class FormatStringResource(
private val format: String,
private val params: List<Any>
) : StringResource {
companion object {
fun create(
val format: String,
vararg params: Any
): ResStringResource {
return FormatStringResource(format, listOf(*params))
}
}
override fun getString(context: Context): String {
val params = params.map {
if (it is StringResource) {
it.getString(context)
} else {
it
}
}
return format.format(*params.toTypedArray())
}
}
これらを組み合わせることで、上記のような複雑な文字列も以下のように表現ができます。
val a = ResStringResource.create(R.string.a)
val b = ResStringResource.create(R.string.b)
val text1 = FormatStringResource.create("%s:%s", a, b)
val param = ResStringResource.create(R.string.param)
val text2 = ResStringResource.create(R.string.text2, param)
今回はあくまでActivity contextを使いつつViewModelで文字列整形をする方法について紹介をしました。
冒頭で述べたとおり、Application contextを使ってしまう方法や、ViewModelではenum等の抽象化されたデータのみを扱うようにする方法等、解決策は色々あると思います。
また、ViewModel以下のlayerでどう扱うのか、みたいな問題も出てくると思います。(今回の方法でも解決できると思います。)
今回紹介した方法が、なにか参考になれば幸いです。