The Problem
You've gotten all the logic tested. Now you just need to turn that last
enum class TimesOfDay(@StringRes val displayNameStringId: Int) {
MORNING(R.string.morning),
NIGHT(R.string.night)
}
to the text "Morning Sunshine!" or "Good night sweetie!"
But to do it in such a way that it can be translated to other languages easily, you can't just use a string. You'd need to go through the Resources, or Context's getString methods.
Drat!
The Solution
DI to the rescue!
Following a standard DI pattern, we're going to put the real Android implementation behind an interface and inject that interface into the place we need it!
Interface
We start off with the basic interface we need
import androidx.annotation.StringRes
interface TranslationStringMapper {
fun getStringForId(@StringRes id: Int): String
}
This ensures you'll only be passing String Resource ids in there, not just any old Int in production.
Concrete Class for the Interface
This class extends the interface and provides the real version that we'll be using in production.
import android.app.Application
import android.content.res.Resources
import javax.inject.Inject
class AndroidTranslationStringMapper @Inject constructor(application: Application) :
TranslationStringMapper {
val res: Resources = application.resources
override fun getStringForId(id: Int): String = res.getString(id)
}
Injecting the interface where needed
You may not need this if you're not using View Models, but the idea can be adapted to presenters just as well.
class EmotionJourneyViewModelFactory @Inject constructor(
private val emotionDao: EmotionDao,
private val stringMapper: TranslationStringMapper
) : ViewModelProvider.Factory { ... }
This will get you the TranslationStringMapper to the ViewModel, or you can directly inject it into the Presenter if that's what you use.
Where do you get the TranslationStringMapper?
Now, there are a few ways to do this but here's one I like the most.
All we need, is a to use the AndroidTranslationStringMapper from earlier, and tell Dagger that we want to use this wherever it's needed.
But the right place to do this, something that could be used app-wide and is just a cast from the concrete to the interface, is....
AppModule!
If you're used to Dagger you'd have seen the AppModule. It's usually binding the application to a context.
Binds, should be preferred when you're just casting from one object to another and that's exactly what we're doing here!
The scope is perfect too since this truly could be used anywhere in the app, and it'll be super easy to change the concrete implementation of the StringMapper in one centralized location rather than each individual activity or fragment module.
That looks like:
@Module
abstract class AppModule {
@Binds
abstract fun bindApplication(app: DaggerApplication): Application
...
@Binds
abstract fun bindStringMapper(androidTranslationStringMapper: AndroidTranslationStringMapper): TranslationStringMapper
}
Conclusion
That's it. Now the translations will be available wherever you need them and you can even quickly mock out responses if you wanted to, while always using just the enum representation in tests if that's what you prefer.
This is really great use of Dagger that lets you forget about where the translation is coming from.
Notes: It's really important that you use the resources from Application rather than an activity context if you're going to pass it into a ViewModel. The ViewModel is preserved beyond the Activity lifecycle and waaay past the fragment lifecycle and using either of these could cause serious problems like memory leaks and exceptions.
I'm currently looking for a job as a senior or lead android developer so if you're hiring and want someone to supercharge your teams and raise everyone up, while learning from them too. Reach out to me at anikadamg att gmail!
Top comments (3)
How about string resource with format arguments?
Add another function to the interface which takes the params you'll need.
Expand as you need things!
But this won’t be type-safe if you just add a vararg at the end and you lose the lint check you get when doing this directly with context.getString(...).
Or do you mean adding a function for each string resource that takes some argument(s)? Wouldn’t this make the mapper not generic any more and you’ll have to maintain / test it often?
I’ve given up messing with string / any other resources in ViewModel (AndroidViewModel sucks) directly 😅 and instead just expose streams of states for Fragment / Activity to render which includes mapping the states to the correct string / color resources etc. This way you don’t need to mock your string mapper in your ViewModelTest and your UI tests will cover your string resource loading anyway.