Dans nos projets, nous essayons de couvrir le code avec des tests selon les besoins et d'adhérer aux principes d'architecture SOLIDE et propre. Nous aimerions partager avec les lecteurs de Habr la traduction de l'article de Hannes Dorfmann, l'auteur d'une série de publications sur le développement Android. Cet article décrit une technique qui peut vous aider à résumer l'utilisation des chaînes pour masquer les détails de l'interaction avec différents types de ressources de chaîne et faciliter l'écriture de tests unitaires.
Si vous travaillez sur une grande application Android et que vous pensez que votre code peut être confus lorsque vous travaillez avec des ressources de différentes sources, ou si vous souhaitez simplifier l'écriture de tests sur des chaînes, cet article peut vous être utile. Traduit avec l'autorisation de l'auteur.
. , 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_text – Int). -:
@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, , . , . , – , , . !