Androidで文字列を操作するための適切な抽象化を見つける方法

私たちのプロジェクトでは、必要に応じてコードをテストでカバーし、SOLIDとクリーンなアーキテクチャの原則を順守しようとしています。Android開発に関する一連の出版物の著者であるHannesDorfmannによる記事の翻訳をHabrの読者と共有したいと思います。この記事では、文字列の操作を抽象化して、さまざまなタイプの文字列リソースとのやり取りの詳細を非表示にし、単体テストを簡単に作成できるようにする手法について説明します。 





大規模なAndroidアプリケーションで作業していて、さまざまなソースのリソースを操作するときにコードが混乱する可能性がある場合、または文字列に対するテストの記述を簡素化したい場合は、この記事が役立つ場合があります。著者の許可を得て翻訳。





写真:スプラッシュ解除
写真:スプラッシュ解除

. , 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