Dê adeus ao strings.xml com Jetpack Compose
Jetpack Compose, o novo UI Toolkit para Android, trouxe grandes — e muito bem vindos — avanços na forma como construímos UIs e lidamos com resources em nossos projetos.
Deixamos de usar arquivos XML — com toda sua verbosidade e limitações — e passamos a utilizar Kotlin para declarar boa parte de nossos resources. Alguns benefícios:
colors.xml
: antes estávamos limitados a declarar cores no formato Hexadecimal, mas agora também é possível declará-las em RGB, um formato bastante usado por designers que pode facilitar na comunicação entre times;styles.xml
: quem nunca se confundiu na hora de usar os atributosandroid:theme
oustyle
? Ou sofreu para criar umDialog
com bordas arredondadas? Agora ficou bem mais simples de criar temas do jeito que precisamos;dimens.xml
: além de usarsp
para tamanho do texto, agora também podemos usarem
, outra medida bastante utilizada por designers;layouts/*.xml
: por fim, e o mais óbvio, toda a UI passa a ser construída com Kotlin idiomático, tirando proveito de todos os recursos que a linguagem oferece.
Sentiu falta de alguém nessa lista? Eu também! Um tipo de resource que — até então — não foi beneficiado com a chegada do Compose foi o strings.xml
. Mas será que vale a pela declarar strings com Kotlin?
🤔 Por que?
Assim como os outros resources são beneficiados pelo Kotlin, não é diferente para strings. A Standard Library do Kotlin possui diversas funções utilitárias que simplificam a manipulação de strings, além de recursos da própria linguagem como string literals e string templates.
Vamos falar sobre algumas vantagens a seguir.
Texto com múltiplas linhas
No XML podemos usar o \n
para quebra de linhas, nada de novo aqui. Porém a indentação do texto pode ser um problema:
<string name="multi_line">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n
Nunc non metus vitae dui faucibus mattis.\n
Praesent pharetra, dui eget imperdiet pellentesque
</string>
Notem que há um espaço em branco após a primeira linha:
Isso pode ser facilmente resolvido colocando todo o texto na mesma linha, entretanto fica mais difícil de ler:
<string name="multi_line">Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nNunc non metus vitae dui faucibus mattis.\nPraesent pharetra, dui eget imperdiet pellentesque</string>
Com string literals temos o controle da indentação e não precisamos quebrar a linha manualmente:
val multiLine = """
|Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|Nunc non metus vitae dui faucibus mattis.
|Praesent pharetra, dui eget imperdiet pellentesque
""".trimMargin()
O resultado:
Texto com argumentos
É simples declarar argumentos em strings com XML:
<string name="message">
Hello, %1$s! You have %2$d new messages.
</string>
Mas não temos como saber — sem olhar para o valor do resource — quantos argumentos devemos passar e de quais tipos (String
ou Int
?):
// Válido
stringResource(R.string.message, "John", 3)// Não passar argumentos é permitido, porém inválido
stringResource(R.string.message)// Passar mais argumentos do que o necessário é permitido, porém inválido
stringResource(R.string.message, "John", 3, "Another text", 99)// Passar argumentos de tipos diferentes é permitido, porém inválido
stringResource(id = R.string.params, 3, "John")
Com o uso de function types (ou lambdas) é possível dar nome aos argumentos e torná-los typesafe:
val message: (name: String, count: Int) -> String = { name, count ->
"Hello, $name! You have $count new messages."
}
Olha só que legal: a IDE irá mostrar o nome dos argumentos como dica, além de lançar erro de compilação caso o tipo esteja errado. Muito melhor, não?
Plurais
Com XML estamos limitados a quantidades fixas para declarar os valores dos plurais, também não é possível reusar o texto em comum:
<plurals name="apples">
<item quantity="one">I have a few apple</item>
<item quantity="two">I have a few apples</item>
<item quantity="few">I have a bunch of apples</item>
<item quantity="other">I have a lot of apples</item>
</plurals>
Com o uso da expressão when temos mais flexibilidade para tratar os plurais, podendo combinar as condições, usar ranges e smart cast, além de reusar o texto em comum:
val apples: (count: Int) -> String = { count ->
val value = when (count) {
1, 2 -> "a few apple"
in 3..10 -> "a bunch of apples"
else -> "a lot of apples"
}
"I have $value"
}
E tem mais!
Poderia continuar dando vários outros exemplos detalhados mas, para não estender muito, aqui vai um resumo:
string-arrays
podem ser substituídos porList
,Set
,Sequence
ou até mesmo outras estruturas de dados comoMap
ePair
;- A formatação do texto através do
AnnotatedString
é mais poderosa do que o suporte limitado as tags do HTML ou a complexa formatação através doannotation
; - Pelo fato das strings estarem declaradas no Kotlin, por que não buscá-las através de uma API? Sim, estou falando de textos dinâmicos! Publicou uma versão com o texto errado? Corrige no backend sem precisar publicar uma nova versão;
- Sabia que o Compose já funciona no Desktop e Web? Cada plataforma armazena strings de um jeito diferente, mas ao declará-las no Kotlin é possível reusá-las em todas as plataformas sem dificuldade.
🤩 Como?
Já sabemos as vantagens em declarar strings com Kotlin em comparação ao XML, mas como usá-las no Compose? É bem simples na verdade.
Vamos começar definindo o nome e o tipo de nossas strings. No exemplo abaixo, estou utilizando um data class
, mas também funciona com class
ou interface
.
data class Strings(
val simple: String,
val annotated: AnnotatedString,
val parameter: (locale: String) -> String,
val plural: (count: Int) -> String,
val list: List<String>
)
Notem que o tipo não necessariamente precisa ser String
, podemos usar AnnotatedString
caso o texto precise ser formatado, alguma Collection
como List
ou Map
, ou até mesmo lambdas!
O próximo passo é instanciar nossa classe Strings
e definir o valor para cada propriedade. Se seu projeto suporta mais de um idioma, cada instância deStrings
deverá ser nomeada de acordo com o idioma, a fim de facilitar a identificação.
val EnStrings = Strings(
simple = "Hello Compose!",
annotated = buildAnnotatedString {
withStyle(SpanStyle(color = Color.Red)) {
append("Hello ")
}
withStyle(SpanStyle(fontWeight = FontWeight.Light)) {
append("Compose!")
}
},
parameter = { locale ->
"Current locale: $locale"
},
plural = { count ->
val value = when (count) {
1, 2 -> "few"
in 3..10 -> "bunch of"
else -> "lot of"
}
"I have a $value apples"
},
list = listOf("Avocado", "Pineapple", "Plum")
)
Chegou a hora de integrar com o Compose. A melhor maneira de tornar nossas strings acessíveis dentro de uma composable function é através do CompositionLocal
. Vamos usar o staticCompositionLocalOf()
pelo fato das strings não mudarem com muita frequência.
val LocalStrings = staticCompositionLocalOf { EnStrings }
Finalmente, podemos acessar nossas strings sem mais depender do strings.xml
e com todos os benefícios do Kotlin 🎉
@Composable
fun StringsSample() {
val strings = LocalStrings.current
Text(text = strings.simple)
Text(text = strings.annotated)
Text(text = strings.parameter("en"))
Text(text = strings.plural(1))
Text(text = strings.plural(5))
Text(text = strings.plural(20))
Text(text = strings.list.joinToString())
)
Resultado:
🌎 Introduzindo Lyricist
Como vimos, é bem simples de declarar strings com Kotlin e usar no Compose. As coisas ficam um pouco mais complicadas quando precisamos dar suporte a I18N e L10N ou temos que migrar o strings.xml
para esse novo formato.
Por esses motivos, decidi desenvolver o Lyricist, uma biblioteca open source (sob a licença MIT) que busca preencher esta lacuna que o Jetpack Compose deixou em aberto.
Com a ajuda do Kotlin Symbol Processing (KSP), o Lyricist é capaz de gerar código responsável por gerenciar múltiplos idiomas e também migrar automaticamente o strings.xml
para Kotlin.