String Resource Provider Using Dependency Inversion In Android
Whatever architecture you are using in your application, the high-level components shouldn’t have a direct dependency on Android classes like context, fragment, resources or others because they should be reusable and unaffected by changes in lower level components or Android classes.
A common use case is constructing objects in a application-level layer that needs access to string resources. There are several solutions to keep the layer free from context or other Android objects, but we will cover a resource provider that will have an interface in your high-level classes that has an implementation containing a Resources object, basically decoupling the layer from Android dependencies, or infra layer, by inverting the dependency.
Please note that this implementation is for demonstrating the power of the Dependency Inversion Principle by retrieving a string with a parameter. Also take into account locale configurations changes as detailed here, and for simple use cases involving only a resource id, you should pass it directly to the view layer.
Dependency Inversion Principle short summary
The Dependency Inversion Principle (DIP) states that:
“high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.”
This help us to decouple high-level layer from Android or infra layer.
Our sample use case
Suppose we have an use case that needs to access a string from android resources and compose a message, let’s name it: GetWelcomeMessageUseCase.
import android.content.res.Resources
class GetWelcomeMessageUseCase(private val resources: Resources) {
fun get(userFullName: String): String =
resources.getString(R.string.welcome_message, userFullName)
}
Removing Android dependency
To remove android.content.res.Resources dependency from this use case we will use an interface, StringResourceProvider:
import androidx.annotation.StringRes
interface StringResourceProvider {
fun getString(@StringRes resourceId: Int, parameter: String): String
}
Refactoring GetWelcomeMessageUseCase
Next we will refactor GetWelcomeMessageUseCase by replacing Resources with our newly created interface and that’s it.
class GetWelcomeMessageUseCase(private val stringResourceProvider: StringResourceProvider) {
fun get(userFullName: String): String =
stringResourceProvider.getString(R.string.welcome_message, userFullName)
}
Now our application-level class, GetWelcomeMessageUseCase, depends upon an abstraction, StringResourceProvider interface, and not Android component.
We have a Clean Code now! 🤩
StringResourceProvider implementation
And the implementation of StringResourceProvider interface:
import android.content.res.Resources
class StringResourceProviderImplementation(private val resources: Resources) :
StringResourceProvider {
override fun getString(resourceId: Int, parameter: String): String =
resources.getString(resourceId, parameter)
}
ViewModel with GetWelcomeMessageUseCase
Following is the full integration of the mentioned use case in a ViewModel:
class WelcomeViewModel(private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase) :
ViewModel() {
val welcomeMessageLiveData = MutableLiveData<String>()
private val userName = "Mihai"
fun displayWelcomeMessage() {
welcomeMessageLiveData.value = getWelcomeMessageUseCase.get(userName)
}
}
Full code integration
ViewModel will be available in your Fragment or Activity using a ViewModelFactory. Please note that for a clean production code in a large app you should use a Dependency Injection framework or Service Locator pattern. Also ViewModel creation is different and you should follow ViewModel’s documentation.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModelFactory =
MyViewModelFactory(StringResourceProviderImplementation(resources))
val welcomeViewModel = viewModelFactory.create(WelcomeViewModel::class.java)
welcomeViewModel.welcomeMessageLiveData.observe(this) { welcomeMessage ->
Log.d(MainActivity::class.java.simpleName, welcomeMessage)
}
welcomeViewModel.displayWelcomeMessage()
}
}
class MyViewModelFactory(private val stringResourceProvider: StringResourceProvider) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T =
WelcomeViewModel(GetWelcomeMessageUseCase(stringResourceProvider)) as T
}
Unit testing
By using a fake StringResourceProvider as a test double, we could easily assert that our GetWelcomeMessageUseCase implementation is correct.
class GetWelcomeMessageUseCaseTest {
private val fakeStringResourceProvider = object : StringResourceProvider {
override fun getString(resourceId: Int, parameter: String): String = when (resourceId) {
R.string.welcome_message -> String.format("Test %s", parameter)
else -> ""
}
}
private val getWelcomeMessageUseCase = GetWelcomeMessageUseCase(fakeStringResourceProvider)
@Test
fun displayWelcomeMessage() {
val name = "Badea Mihai Bogdan"
val welcomeMessage = getWelcomeMessageUseCase.get(userFullName = name)
Assert.assertEquals("Test Badea Mihai Bogdan", welcomeMessage)
}
}
Conclusion
We have seen that by using the Dependency Inversion Principle we can achieve cleaner code, improve maintainability and not pollute the application layer with Android framework specific software components, also it allows us to unit test without Android dependencies.
You could follow me also on Twitter for more tech related subjects.