Jetpack ViewModel and string resources

When writing my ViewModels I try hard to make them as Android-independent as possible. This means that, in the ideal scenario, I don't want them to depend on any Android system class.

The reasoning behind this approach is that a ViewModel should contain pure business logic. It shouldn't  have to depend on system methods of any kind. In theory, this ViewModel should be able to be reused in a different platform that just implements a different system-dependent view.

But in the real world, that's not so easy. The most common Android system dependency for a ViewModel, is the string resources. You almost certainly will need to return a conditional string (e.g. failure type message). So how do you keep the ViewModel "independence" in this case?

Use the resource IDs

The official recommended way, is to use the String resource IDs. Although not "technically" part of the Android system, this approach will require the Android build tools to generate those IDs.

val errorMessageLiveData = MutableLiveData<Int>()
[...]
errorMessageLiveData.value = R.string.error_message_1

Then in your View, you would actually use Context to retrieve those Strings, keeping your ViewModel Android-free.

errorMessageLiveData.observe(this, Observer {
        Toast.makeText(context, getString(it), Toast.LENGTH_LONG).show()   
    }
)

Use a resources "helper"

Create a ResourceHelper class that depends on Android's Context. The sole purpose of the class is to return string messages, abstracting the dependence on the system.

class ResourcesHelper(private val applicationContext: Context) {

    val errorMessage
        get() = applicationContext.getString(R.string.error_message)
}
Example of a "ResourceHelper" 

You would then inject this into your ViewModel, keeping it "clean" from any system dependencies. You can easily mock/fake this, to return sample strings in your tests.

Of course, this is the "cleanest" solution in my opinion but comes with the biggest overhead, since you will need to add a new getter/method for each new string. Another caveat, is that the application Context will not reflect any changes in the devices locale.

Use the Context

Now if you decide that you don't care about this Android-system-independence nonsense and you just want to use the Context, how do you get such a reference in your ViewModel?

One way is to use AndroidViewModel (instead of ViewModel) as a base class.

class MyViewModel(private val application: Application) : AndroidViewModel(application) {
    val errorMessage
        get() = application.resources.getString(R.string.some_string)
}

Another way would be to keep your ViewModel base class and inject the Application Context to get a Resources reference.

This would be the simplest and easiest approach but might cause some issues when the users switch language + you would need to provide a Context instance in your tests (using Robolectric for instance).

Hopefully, this overview helped on how to handle strings in your ViewModels. Happy Android coding!