How to find the right abstraction for working with strings in Android

In our projects, we try to cover the code with tests as needed and adhere to the principles of SOLID and clean architecture. We would like to share with Habr's readers the translation of the article by Hannes Dorfmann, the author of a series of publications about Android development. This article describes a technique that can help you abstract away working with strings to hide the details of interacting with different types of string resources and make it easier to write unit tests. 





If you are working on a large Android application and you suspect that your code might get confused when working with resources from different sources, or if you want to simplify writing tests against strings, then this article may be useful to you. Translated with the permission of the author.





Photo: Unsplash
Photo: Unsplash

. , Android Android.





?

Android? , , . , , , , , , . , : 





  • R.string.some_text, resources.getString(R.string.some_text)





  • , , .. context.getString(R.string.some_text, «arg1», 123)  





<string name=”some_formatted_text”>Some formatted Text with args %s %i</string>
      
      



  • , Plurals, , resources.getQuantityString(R.plurals.number_of_items, 2)





<plurals name="number_of_items">
  <item quantity="one">%d item</item>
  <item quantity="other">%d items</item>
</plurals>
      
      



  • , Android XML- strings.xml, String ( R.string.some_text). , , json .





, , ? . :





 1. , , .





 2. ( ) -, , . 





: , http, , fallback- strings.xml. , :





class MyViewModel(
  private val backend : Backend,
  private val resources : Resources //  Android  context.getResources()
) : ViewModel() {
  val textToDisplay : MutableLiveData<String>  // MutableLiveData    
 
  fun loadText(){
    try {
      val text : String = backend.getText() 
      textToDisplay.value = text
    } catch (t : Throwable) {
      textToDisplay.value = resources.getString(R.string.fallback_text)
    }
  }
}
      
      



MyViewModel, . , loadText(), Resources, StringRepository ( ""), : 





interface StringRepository{
  fun getString(@StringRes id : Int) : String
}
 
class AndroidStringRepository(
  private val resources : Resources //  Android  context.getResources()
) : StringRepository {
  override fun getString(@StringRes id : Int) : String = resources.getString(id)
}
 
class TestDoubleStringRepository{
    override fun getString(@StringRes id : Int) : String = "some string"
}
      
      



- StringRepository , , ?





class MyViewModel(
  private val backend : Backend,
  private val stringRepo : StringRepository //     
) : ViewModel() {
  val textToDisplay : MutableLiveData<String>  
 
  fun loadText(){
    try {
      val text : String = backend.getText() 
      textToDisplay.value = text
    } catch (t : Throwable) {
      textToDisplay.value = stringRepo.getString(R.string.fallback_text)
    }
  }
}
      
      



- -:





@Test
fun when_backend_fails_fallback_string_is_displayed(){
  val stringRepo = TestDoubleStringRepository()
  val backend = TestDoubleBackend()
  backend.failWhenLoadingText = true //  backend.getText()  
  val viewModel = MyViewModel(backend, stringRepo)
  viewModel.loadText()
 
  Assert.equals("some string", viewModel.textToDisplay.value)
}
      
      



interface StringRepository , ? . , : 





  • StringRepository , (. ). , - , , String. .





  • ,   TestDoubleStringRepository , , ? TestDoubleStringRepository . -, R.string.foo R.string.fallback_text StringRepository.getString(), . , TestDoubleStringRepository, :





class TestDoubleStringRepository{
    override fun getString(@StringRes id : Int) : String = when(id){
      R.string.fallback_test -> "some string"
      R.string.foo -> "foo"
      else -> UnsupportedStringResourceException()
    }
}
      
      



? ( )?





, .





TextResource 

TextResource. , domain. , -. :





sealed class TextResource {
  companion object { //     ,       
    fun fromText(text : String) : TextResource = SimpleTextResource(text)
    fun fromStringId(@StringRes id : Int) : TextResource = IdTextResource(id)
    fun fromPlural(@PluralRes id: Int, pluralValue : Int) : TextResource = PluralTextResource(id, pluralValue)
  }
}
 
private data class SimpleTextResource( //     inline 
  val text : String
) : TextResource()
 
private data class IdTextResource(
  @StringRes id : Int
) : TextResource()
 
private data class PluralTextResource(
    @PluralsRes val pluralId: Int,
    val quantity: Int
) : TextResource()
 
//       
...
      
      



  - TextResource:





class MyViewModel(
  private val backend : Backend // , , ,      - ,  StringRepository.
) : ViewModel() {
  val textToDisplay : MutableLiveData<TextResource> //    String  
 
  fun loadText(){
    try {
      val text : String = backend.getText() 
      textToDisplay.value = TextResource.fromText(text)
    } catch (t : Throwable) {
      textToDisplay.value = TextResource.fromStringId(R.string.fallback_text)
    }
  }
}
      
      



:





1) textToDisplay c LiveData<String> LiveData<TextResource>, - , String.   TextResource. , , , TextResource – , . 





2) -. « » StringRepository ( Resources). , , , ? , TextResource. , Android, (R.string.fallback_textInt). -: 





@Test
fun when_backend_fails_fallback_string_is_displayed(){
  val backend = TestDoubleBackend()
  backend.failWhenLoadingText = true //  backend.getText()  
  val viewModel = MyViewModel(backend)
  viewModel.loadText()
 
  val expectedText = TextResource.fromStringId(R.string.fallback_text)
  Assert.equals(expectedText, viewModel.textToDisplay.value)
  //  data class-   equals,      
}
      
      



, : TextResource String, , , TextView? , Android, UI. 





//      context.getResources()
fun TextResource.asString(resources : Resources) : String = when (this) { 
  is SimpleTextResource -> this.text // smart cast
  is IdTextResource -> resources.getString(this.id) // smart cast
  is PluralTextResource -> resources.getQuantityString(this.pluralId, this.quantity) // smart cast
}
      
      



TextResource String UI ( ) , TextResource «» (.. ), R.string.*





: - TextResource.asString(), . , when. resources.getString(). , TextResource , «/». , , : , TextResource, when TextResource.asString()





: , TextResource /. / TextResource, sealed class TextResouce abstract fun asString(r: Resources), . , / asString(r: Resources), ( , , /). ? , Resources API TextResource , (, SimpleTextResource ). , API, , ( ). 





: dimens, , . , . , – , , . !








All Articles