Dê adeus ao strings.xml com Jetpack Compose

Adriel Café
stonetech
Published in
6 min readJun 17, 2021

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:themeou style? Ou sofreu para criar um Dialogcom bordas arredondadas? Agora ficou bem mais simples de criar temas do jeito que precisamos;
  • dimens.xml: além de usar sp para tamanho do texto, agora também podemos usar em , 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 por List, Set, Sequence ou até mesmo outras estruturas de dados como Map e Pair;
  • 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.

--

--