O Jetpack Compose foi criado com base em Kotlin. Em alguns casos, o Kotlin oferece expressões idiomáticas especiais que facilitam a criação de um bom código do Compose. Se você imaginar seu código em outra linguagem de programação e traduzir mentalmente essa linguagem para o Kotlin, provavelmente perderá algumas das vantagens do Compose e poderá achar difícil entender código Kotlin escrito idiomaticamente. Compreender melhor o estilo do Kotlin pode ajudar você a evitar esses problemas.
Argumentos padrão
Ao escrever uma função do Kotlin, você pode especificar valores padrão para argumentos de função, usados se o autor da chamada não transmitir esses valores explicitamente. Esse recurso reduz a necessidade de funções sobrecarregadas.
Por exemplo, suponha que você queira criar uma função que desenhe um quadrado. Essa função pode ter um único parâmetro obrigatório, sideLength, especificando o comprimento de cada lado. Ela pode ter vários parâmetros opcionais, como thickness, edgeColor e assim por diante. Se eles não forem especificados pelo autor da chamada, a função usará valores padrão. Em outras linguagens, seria esperado ter que criar várias funções:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
Com o Kotlin, é possível escrever uma única função e especificar os valores padrão dos argumentos:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
Além de evitar que você precise escrever várias funções redundantes, esse
recurso deixa a leitura do código muito mais clara. Caso o autor da chamada não especifique um
valor para um argumento, isso indica que ele está disposto a usar o valor
padrão. Além disso, os parâmetros nomeados facilitam muito a visualização do que está
acontecendo. Se você observar o código e vir uma chamada de função como esta, talvez não
saiba o que os parâmetros significam sem analisar o código drawSquare()
:
drawSquare(30, 5, Color.Red);
Por outro lado, este código é autodocumentado:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
A maioria das bibliotecas do Compose usa argumentos padrão, e é recomendável fazer o mesmo para as funções que podem ser compostas que você criar. Essa prática torna os elementos que podem ser compostos personalizáveis, mas ainda será simples invocar o comportamento padrão. Por exemplo, você pode criar um elemento de texto simples como este:
Text(text = "Hello, Android!")
Esse código tem o mesmo efeito que o seguinte código muito mais detalhado, em que
mais parâmetros
Text
são definidos explicitamente:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Além do primeiro snippet de código ser muito mais simples e fácil de ler, ele também
é autodocumentado. Ao especificar apenas o parâmetro text
, você documenta que, para
todos os outros parâmetros, você quer usar os valores padrão. Já o
segundo snippet implica que você quer definir explicitamente os valores desses
outros parâmetros, ainda que os valores definidos sejam os valores padrão
da função.
Funções de ordem superior e expressões lambda
O Kotlin é compatível com funções de
ordem superior, que
recebem outras funções como parâmetros. O Compose amplia essa abordagem. Por
exemplo, a
função que pode ser composta Button
fornece um parâmetro lambda onClick
. O valor
desse parâmetro é uma função, que o botão chama quando o usuário clica nele:
Button( // ... onClick = myClickFunction ) // ...
As funções de ordem superior são pareadas naturalmente com expressões lambda, que são expressões
que avaliam uma função. Se você precisar da função apenas uma vez, não precisará
defini-la em outro lugar para transmiti-la à função de ordem superior. Em vez disso, você pode
definir a função diretamente com uma expressão lambda. O exemplo anterior
supõe que myClickFunction()
foi definida em outro lugar. Mas, se você usar essa
função apenas aqui, será mais simples definir a função in-line com uma expressão
lambda:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Lambdas finais
O Kotlin oferece uma sintaxe especial para chamar funções de ordem superior cujo parâmetro last é uma lambda. Se você quiser transmitir uma expressão lambda como esse parâmetro, poderá usar a sintaxe de lambdas finais. Em vez de colocar a expressão lambda entre parênteses, você a coloca em seguida. Essa é uma situação comum no Compose, então você precisa estar familiarizado com a aparência do código.
Por exemplo, o último parâmetro para todos os layouts, como a
função que pode ser composta
Column()
, é content
, uma função que emite os elementos de IU
filhos. Suponha que você queira criar uma coluna com três elementos de texto
e aplicar uma formatação. Esse código funcionaria, mas seria muito
complicado:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Como o parâmetro content
é o último na assinatura da função e
estamos transmitindo o valor dele como uma expressão lambda, podemos retirá-lo dos
parênteses:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Os dois exemplos têm o mesmo significado. As chaves definem a expressão
lambda transmitida para o parâmetro content
.
Na verdade, se o único parâmetro que você está transmitindo for a lambda final, ou seja,
se o parâmetro final for uma lambda e você não estiver transmitindo outros
parâmetros, você poderá omitir os parênteses. Então, por exemplo, suponha que você
não tenha transmitido um modificador para Column
. Você pode escrever o código
assim:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Essa sintaxe é muito comum no Compose, especialmente para elementos de layout, como
Column
. O último parâmetro é uma expressão lambda que define os filhos
do elemento, e eles são especificados em chaves após a chamada de função.
Escopos e receptores
Alguns métodos e propriedades só estão disponíveis em escopos específicos. O escopo limitado permite oferecer funcionalidades quando necessário e evitar a utilização acidental delas quando não for apropriado.
Considere um exemplo usado no Compose. Quando você chama o layout Row
que pode ser composto, o lambda de conteúdo é automaticamente invocado em RowScope
.
Isso permite que Row
exponha funcionalidades válidas somente dentro de um Row
.
O exemplo abaixo demonstra como Row
expôs um valor específico de linha para
o modificador align
:
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
Algumas APIs aceitam lambdas chamadas no escopo do receptor. Essas lambdas têm acesso a propriedades e funções que são definidas em outro lugar, com base na declaração de parâmetro:
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
Para ver mais informações, consulte literais de função com o receptor na documentação do Kotlin (link em inglês).
Propriedades delegadas
O Kotlin é compatível com propriedades
delegadas.
Essas propriedades são chamadas como se fossem campos, mas o valor delas é
determinado dinamicamente pela avaliação de uma expressão. É possível reconhecer essas
propriedades pelo uso da sintaxe by
:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Outros códigos podem acessar a propriedade com um código como este:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
Quando println()
é executado, nameGetterFunction()
é chamada para retornar o valor
da string.
Essas propriedades delegadas são úteis principalmente quando você trabalha com propriedades com estado armazenado:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Como desestruturar classes de dados
Se você definir uma classe
de dados, será possível acessar
os dados facilmente com uma declaração
de desestruturação. Por
exemplo, suponha que você defina uma classe Person
:
data class Person(val name: String, val age: Int)
Se você tiver um objeto desse tipo, poderá acessar os valores dele com um código como este:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Você geralmente verá esse tipo de código em funções do Compose:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
As classes de dados oferecem muitas outras funcionalidades úteis. Por exemplo, quando você
define uma classe de dados, o compilador define automaticamente funções úteis, como
equals()
e copy()
. Veja mais informações na documentação das classes
de dados.
Objetos singleton
Com o Kotlin, é fácil declarar singletons, que sempre têm uma e
apenas uma instância. Estes singletons são declarados com a palavra-chave object
.
O Compose usa esses objetos com frequência. Por exemplo, o
MaterialTheme
é
definido como um objeto singleton; as propriedades MaterialTheme.colors
, shapes
e
typography
contêm os valores do tema atual.
Builders com segurança de tipos e DSLs
O Kotlin permite criar linguagens específicas do domínio (DSLs) com builders com segurança de tipos. As DSLs permitem criar estruturas hierárquicas de dados complexas de maneira mais sustentável e legível.
O Jetpack Compose usa DSLs para algumas APIs, como
LazyRow
e LazyColumn
.
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
O Kotlin garante a segurança de tipo dos builders usando
literais de função com receptor (link em inglês).
Se usarmos a função que pode ser composta Canvas
como exemplo, ela terá como parâmetro uma função com
DrawScope
como receptor, onDraw: DrawScope.() -> Unit
, permitindo que o bloco de código
chame funções de membro definidas em DrawScope
.
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
Saiba mais sobre builders de tipo seguro e DSLs na documentação do Kotlin.
Corrotinas do Kotlin
As corrotinas oferecem compatibilidade com programação assíncrona no nível da linguagem em Kotlin. As corrotinas podem suspender a execução sem bloquear linhas de execução. Uma IU responsiva é inerentemente assíncrona, e o Jetpack Compose resolve isso adotando corrotinas no nível da API em vez de usar callbacks.
O Jetpack Compose oferece APIs que tornam o uso de corrotinas seguro na camada da IU.
A função rememberCoroutineScope
retorna um CoroutineScope
que você pode usar para criar corrotinas em manipuladores de eventos e chamar
APIs de suspensão do Compose. Veja o exemplo abaixo usando a API
animateScrollTo
do
ScrollState
.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
As corrotinas executam o bloco de código de forma sequencial por padrão. Uma corrotina
em execução que chama uma função de suspensão suspenderá a execução até que
a função de suspensão seja retornada. Isso acontecerá mesmo se a função de suspensão mover a
execução para um CoroutineDispatcher
diferente. No exemplo anterior, o
loadData
não será executado até que a função de suspensão animateScrollTo
retorne.
Para executar o código simultaneamente, é necessário criar novas corrotinas. No exemplo
acima, para carregar a rolagem na parte superior da tela e carregar dados do
viewModel
, duas corrotinas são necessárias.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
As corrotinas facilitam a combinação de APIs assíncronas. No exemplo
a seguir, combinamos o modificador pointerInput
com as APIs de animação para
animar a posição de um elemento quando o usuário toca na tela.
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
Para saber mais sobre corrotinas, consulte o guia Corrotinas do Kotlin no Android.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Componentes e layouts do Material Design
- Efeitos colaterais no Compose
- Conceitos básicos de layout do Compose