Estável e imutável no Jetpack Compose

Tempo de leitura: 5 minutes

Mostrarei a você a estabilidade na composição usando dados mutáveis e imutáveis, anotações e funções que melhoram o desempenho da interface do usuário.

O Compose considera os tipos como estáveis ou instáveis. Um tipo é estável se for imutável ou se for possível para o Compose saber se seu valor foi alterado entre as recomposições. Um tipo é instável se o Compose não puder saber se o seu valor foi alterado entre as recomposições.

O Compose usa a estabilidade dos parâmetros de um composable para determinar se ele pode ignorar o composable durante a recomposição:

  • Parâmetros estáveis: Se um compostável tiver parâmetros estáveis que não foram alterados, o Compose o ignora.
  • Parâmetros instáveis: Se um componente compostável tiver parâmetros instáveis, o Compose sempre o recompõe quando recompõe o componente pai.

Se o seu aplicativo incluir muitos componentes desnecessariamente instáveis que o Compose sempre recompõe, você poderá observar problemas de desempenho e outros problemas.

Este documento detalha como você pode aumentar a estabilidade do seu aplicativo para melhorar o desempenho e a experiência geral do usuário.

 

Immutable objects

Os trechos a seguir demonstram os princípios gerais por trás da estabilidade e da recomposição.

A classe Contact é uma classe de dados imutável. Isso ocorre porque todos os seus parâmetros são primitivos definidos com a palavra-chave val. Depois de criar uma instância de Contact, você não poderá alterar o valor das propriedades do objeto. Se você tentasse fazer isso, criaria um novo objeto.

data class Contact(val name: String, val number: String)

O composable ContactRow tem um parâmetro do tipo Contact.

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }
   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

Considere o que acontece quando o usuário clica no botão de alternância e o estado selected muda:

  1. O Compose avalia se deve recompor o código dentro do ContactRow.
  2. Ele vê que o único argumento para ContactDetails é do tipo Contact.
  3. Como Contact é uma classe de dados imutável, o Compose tem certeza de que nenhum dos argumentos de ContactDetails foi alterado.
  4. Dessa forma, o Compose ignora ContactDetails e não o recompõe.
  5. Por outro lado, os argumentos de ToggleButton foram alterados, e o Compose recompõe esse componente.

 

Objetos mutáveis

Embora o exemplo anterior use um objeto imutável, é possível criar um objeto mutável. Considere o seguinte trecho:

data class Contact(var name: String, var number: String

Como cada parâmetro de Contact agora é uma var, a classe não é mais imutável. Se suas propriedades fossem alteradas, o Compose não se daria conta. Isso ocorre porque o Compose rastreia apenas as alterações nos objetos Compose State.

O Compose considera essa classe instável. O Compose não ignora a recomposição de classes instáveis. Assim, se Contact fosse definido dessa forma, ContactRow no exemplo anterior se recomporia sempre que a selected fosse alterada.

 

Implementação no Compose

Pode ser útil, embora não seja crucial, considerar como exatamente o Compose determina quais funções devem ser ignoradas durante a recomposição.

Quando o compilador do Compose é executado em seu código, ele marca cada função e tipo com uma de várias tags. Essas tags refletem como o Compose lida com a função ou o tipo durante a recomposição.

Observação: Essas tags não são estritamente necessárias para entender a recomposição e a estabilidade, conforme descrito nas seções anteriores deste documento. No entanto, elas são amplamente úteis na depuração de problemas de estabilidade.

 

Funções

O Compose pode marcar funções como ignoráveis ou reiniciáveis. Observe que ele pode marcar uma função como uma, ambas ou nenhuma dessas opções:

  • Ignorável: Se o compilador marcar uma função componível como ignorável, o Compose poderá ignorá-la durante a recomposição se todos os seus argumentos forem iguais aos seus valores anteriores.
  • Reiniciável: Um composable que é reinicializável serve como um “escopo” onde a recomposição pode começar. Em outras palavras, a função pode ser um ponto de entrada no qual o Compose pode começar a reexecutar o código para recomposição após alterações de estado.

Quando você gera o relatório de métricas do Compose (metrics), ele marca os objetos como stable ou unstable; no caso de objetos unstale, o compilador do Compose não consegue saber se o objeto foi modificado, portanto, ele precisa acionar a recomposição independentemente. Aqui estão dois trechos de como o relatório se parece:

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ExampleClass1(
  stable modifier: Modifier? = @static Companion
)

restartable scheme("[androidx.compose.ui.UiComposable]") fun ExampleClass2(
  stable modifier: Modifier? = @static Companion
  stable title: String
  unstable list: List<User>
  stable onClicked: Function1<User>, Unit>
)

 

skippable é desejado!

No caso de ExampleClass1, ela é marcada como skippable, porque todos os seus parâmetros são marcados como stable. No caso da ExampleClass2, ela não é marcada como skippable, porque tem uma lista de propriedades que é unstable.

Quando é marcado como skippable, isso é bom, porque o compilador Compose pode ignorar a recomposição sempre que possível e é mais otimizado.

 

quando ele deixará de ser marcado como ignorável?

Normalmente, o compilador de composição é inteligente o suficiente para deduzir o que é stable e o que é unstable. Nos casos em que o compilador de composição não consegue determinar a estabilidade, trata-se de objetos mutáveis, por exemplo, uma classe que contém propriedades var.

class UIState {
  var isLoading: Boolean
}

Outro caso em que ele falhará em decidir a estabilidade seria para classes como Collection, como List, porque mesmo que a interface seja List, que parece imutável, ela pode, na verdade, ser uma lista mutável. Exemplo:

data class DataList {
    val list: List<String>
}

@Composable
fun ShowData(data: DataList) {
}

Embora o Composable acima aceite DataList em que todas as suas propriedades são val, ele ainda é unstable. Você pode se perguntar por quê? Porque, no lado do uso, você pode realmente usá-lo com uma MutableList, assim:

ShowData(DataList(mutableListOf()))

Por esse motivo, o compilador terá de marcá-lo como unstable.

Portanto, em casos como esse, o que queremos alcançar é torná-los stable novamente, para que sejam otimizados.

 

Tipos

A composição marca os tipos como immutable ou stable. Cada tipo é um ou outro:

  • Immutable: O Compose marca um tipo como immutable se o valor de suas propriedades nunca puder ser alterado e todos os métodos forem referencialmente transparentes. Observe que todos os tipos primitivos são marcados como immutable. Isso inclui String, Int e Float.
  • Stable: Indica um tipo cujas propriedades podem ser alteradas após a construção. Se e quando essas propriedades forem alteradas durante o tempo de execução, o Compose ficará ciente dessas alterações.

Usar @Immutable significa que você sempre fará uma nova cópia dos dados quando passar para o Composable; em outras palavras, você promete que seus dados são imutáveis. No exemplo acima:

@Immutable
data class DataList{
    val list: List<String>
}

@Composable
fun ShowData(data: DataList) {
}

Depois de anotar com @Immutable, em seu lado de uso, você deve se certificar de criar uma nova lista em vez de alterar sua lista diretamente.

Exemplo DO:

class ViewModel {
    var datalist: DataList = DataList(listOf())
    fun removeLastItem() {
        val newList = datalist.list.toMutableList().apply {
                removeLast()
            }
        datalist = datalist.copy(
            list = newList
        )
    }
}

Exemplo: NÃO:

class ViewModel {
    val datalist: DataList= DataList(mutableListOf())
    fun removeLastItem() {
        datalist.list.removeLast() // <=== você violou sua promessa de @Immutable!
    }
}

O uso de @Stable, conforme mencionado acima, significa que o valor pode ser alterado, mas quando ele for alterado, teremos que notificar o compilador Compose. A maneira de fazer isso é usar mutableStateOf():

@Stable
class DataList {
  var someFlag by mutableStateOf(false)
}

Observação: os parâmetros de um composable não precisam ser imutáveis para que o Compose o considere ignorável. Eles podem ser mutáveis, desde que o tempo de execução do Compose seja notificado de todas as alterações. Para a maioria dos tipos, esse seria um contrato impraticável de se manter. No entanto, o Compose fornece classes mutáveis que cumprem esse contrato para você, como MutableState, SnapshotStateMap e SnapShotStateList.

Stable e Immutable, útil em locais como Lazy Column, Row, etc. para melhorar o desempenho da rolagem e reduzir a composição.