Estável e imutável no Jetpack Compose
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.
Conteudo
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:
- O Compose avalia se deve recompor o código dentro do
ContactRow
. - Ele vê que o único argumento para
ContactDetails
é do tipoContact
. - Como
Contact
é uma classe de dados imutável, oCompose
tem certeza de que nenhum dos argumentos deContactDetails
foi alterado. - Dessa forma, o
Compose
ignoraContactDetails
e não o recompõe. - Por outro lado, os argumentos de
ToggleButton
foram alterados, e oCompose
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
eFloat
. - 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.