
Solving AndroidViewModel’s locale changes antipattern
The moment I read the post “Locale changes and the AndroidViewModel antipattern”, I knew it wouldn’t apply to our case, at least not in its current form.
Why? You might ask. Simply, because we use string placeholders extensively and our ViewModels are filled with code similar to the following:
val transferFee: String = application.getString(
R.string.slide_menu_fees_transfer_fee,
minFee
)This exposed string field is then consumed by data binding in the layout XML file.
You might think that it’s simple and we could solve this by exposing the string id instead and calling
context.getString(viewModel.stringId, minFee);from the layout file, but in most cases, the arguments, like minFee in this case, are also locale-dependent strings, which would result in a lot of boilerplate code in the layout XML file.
Furthermore, in certain cases, the previous suggestion won’t do since the arguments are fetched from the server.
The solution is actually similar to the original idea in the mentioned post.
Instead of exposing the id of the string as an Int, we can expose the function getString and the string id alongside its arguments as a class that I’m going to call StringResource.
class StringResource(
val getString: ((Int) -> String),
@StringRes val resId: Int,
vararg val args: Any = emptyArray()
)In the ViewModel, we can now expose the transferFee as follows:
val transferFee : StringResource = StringResource(
application::getString,
R.string.slide_menu_fees_transfer_fee, minFee
)Finally, the missing piece which converts a StringResource to a String is
@BindingConversion
fun getStringFromResource(stringResource: StringResource): String {
val originalString = stringResource.getString(stringResource.resId)
return if (stringResource.args.isNotEmpty()) {
val strings = Array(stringResource.args.size) { index ->
when (val arg = stringResource.args[index]) {
is String -> arg
is StringResource -> getStringFromResource(arg)
else -> throw IllegalAccessException("$arg is not supported")
}
}
String.format(originalString, *strings)
} else {
originalString
}
}You may have noticed that it’s marked as a BindingConversion so we don’t need to change anything in our layout XML file since every time a String is expected but a StringResource is found, the function is automatically invoked by data binding.
This solution supports, not only Strings as string arguments, but also StringResources, so in the case where minFee is also locale-dependent, we can construct transferFee as follows:
val transferFee : StringResource = StringResource(
application::getString,
R.string.slide_menu_fees_transfer_fee,
StringResource(application::getString, R.string.min_fee)
)And voila! We mitigated the locale changes antipattern in the ViewModels while retaining the possibility of using string placeholders and constructing nested strings.
