A migração do Views para o Compose é puramente relacionada à interface, mas há muitos fatores a serem considerados para que a migração seja segura e incremental. Esta página contém algumas considerações para a migração do seu app baseado na visualização para o Compose.
Como migrar o tema do app
O Material Design é o sistema de design recomendado para temas de apps Android.
Para apps baseados em visualização, existem três versões do Material Design disponíveis:
- Material Design 1 usando a biblioteca
AppCompat. Ou seja,
Theme.AppCompat.*
. - Material Design 2 usando a
biblioteca MDC-Android (ou seja,
Theme.MaterialComponents.*
). - Material Design 3 usando a
biblioteca MDC-Android (ou seja,
Theme.Material3.*
).
Para apps do Compose, há duas versões do Material Design disponíveis:
- Material Design 2 usando a
biblioteca Compose Material.
Ou seja,
androidx.compose.material.MaterialTheme
. - Material Design 3 usando a
biblioteca Compose Material 3.
Ou seja,
androidx.compose.material3.MaterialTheme
.
Recomendamos o uso da versão mais recente, o Material 3, se o sistema de design do app tiver condições para isso. Há guias de migração disponíveis para as visualizações e o Compose:
- Material 1 ao Material 2 em visualizações
- Material 2 ao Material 3 em visualizações
- Material 2 ao Material 3 no Compose
Ao criar novas telas no Compose, independente da versão do Material Design
que você está usando, aplique um MaterialTheme
antes de qualquer
elemento combinável que emite a IU das bibliotecas do Compose Material. Os componentes do
Material Design (Button
, Text
etc.) dependem da existência de um MaterialTheme
,
e o comportamento deles fica indefinido sem isso.
Todos os
exemplos do Jetpack Compose
usam um tema personalizado do Compose criado sobre MaterialTheme
.
Consulte Como projetar sistemas no Compose e Como migrar temas XML para o Compose para saber mais.
Navegação
Se você usa o componente de navegação no app, consulte Navegação com o Compose: interoperabilidade e Migrar a navegação do Jetpack para o Navigation Compose para mais informações.
Testar a interface do Compose em conjunto com visualizações
Após migrar partes do seu app para o Compose, os testes são essenciais para garantir que não haja nenhuma falha.
Quando uma atividade ou um fragmento usa o Compose, você precisa usar a
createAndroidComposeRule
em vez da ActivityScenarioRule
. A createAndroidComposeRule
integra a
ActivityScenarioRule
com uma ComposeTestRule
, que permite testar o código do Compose e
da visualização ao mesmo tempo.
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
Consulte Como testar o layout do Compose para saber mais sobre testes. Para interoperabilidade com frameworks de teste de interface, consulte Interoperabilidade com o Espresso e Interoperabilidade com o UiAutomator.
Como integrar o Compose à arquitetura de app que você já usa
Os padrões da arquitetura do fluxo de dados unidirecional (UDF, na sigla em inglês) funcionam perfeitamente com o Compose. Caso o app use outros tipos de padrão de arquitetura, como o Model View Presenter (MVP), recomendamos migrar essa parte da interface para a arquitetura UDF antes ou durante a adoção do Compose.
Como usar um ViewModel
no Compose
Se você usar a biblioteca Architecture Components
ViewModel
, poderá acessar um
ViewModel
em qualquer elemento combinável
chamando a função
viewModel()
,
conforme explicado em Compose e outras bibliotecas.
Ao adotar o Compose, tenha cuidado ao usar o mesmo tipo de ViewModel
em
diferentes elementos que podem ser compostos, considerando que os elementos ViewModel
seguem os escopos do ciclo de vida da visualização. O
escopo será a atividade do host, o fragmento ou o gráfico de navegação se a
biblioteca Navigation for usada.
Por exemplo, se os elementos combináveis forem hospedados em uma atividade, o viewModel()
sempre
vai retornar a mesma instância que só será apagada quando a atividade for concluída.
No exemplo abaixo, o mesmo usuário ("user1") é recebido duas vezes porque
a mesma instância do GreetingViewModel
é reutilizada em todos os elementos combináveis na
atividade do host. A primeira instância do ViewModel
criada é reutilizada em outros
elementos combináveis.
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
Como os gráficos de navegação também incluem o escopo de elementos ViewModel
, os elementos combináveis que são um
destino em um gráfico de navegação têm uma instância diferente do ViewModel
.
Nesse caso, o escopo do ViewModel
é definido como o ciclo de vida do destino e
será apagado quando o destino for removido da backstack. No exemplo
a seguir, quando o usuário navega para a tela Profile, uma nova
instância do GreetingViewModel
é criada.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Fonte da verdade do estado
Quando você adota o Compose em uma parte da interface, é possível que o código do Compose e
do sistema de visualização precisem compartilhar dados. Quando possível, recomendamos
encapsular esse estado compartilhado em outra classe que siga as práticas recomendadas de UDF
usadas pelas duas plataformas, como em um ViewModel
que expõe um stream dos dados
compartilhados para emitir atualizações de dados.
No entanto, isso nem sempre é possível se os dados a serem compartilhados forem mutáveis ou estiverem estreitamente vinculados a um elemento da interface. Nesse caso, um sistema precisa ser a fonte da verdade. Ele também precisa compartilhar as atualizações de dados com o outro sistema. Como regra geral, a fonte da verdade precisa ser de propriedade do elemento que estiver mais próximo da raiz da hierarquia da IU.
Compose como fonte da verdade
Use o
elemento combinável SideEffect
para publicar o estado do Compose em um código que não seja dele. Nesse caso, a
fonte da verdade é mantida em um elemento combinável que envia atualizações de estado.
Por exemplo, sua biblioteca de análise pode permitir segmentar a população
de usuários anexando metadados personalizados (nesse caso, propriedades do usuário)
a todos os eventos de análise subsequentes. Para comunicar o tipo de
usuário atual à biblioteca de análise, use o SideEffect
para atualizar o valor da biblioteca.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
Para mais informações, consulte Efeitos colaterais no Compose.
Sistema de visualização como fonte da verdade
Se o sistema de visualização é proprietário do estado e o compartilha com o Compose, recomendamos que
você una o estado em objetos mutableStateOf
para torná-lo seguro para linhas de execução
no Compose. Se você usar essa abordagem, as funções compostas serão simplificadas, porque
não terão mais a fonte da verdade. Mas o sistema de visualização precisará atualizar o
estado imutável e as visualizações que usam esse estado.
No exemplo abaixo, um CustomViewGroup
contém uma TextView
e uma
ComposeView
com um elemento de composição TextField
. O TextView
precisa mostrar
o conteúdo digitado pelo usuário no TextField
.
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
Como migrar uma IU compartilhada
Caso você esteja migrando gradualmente para o Compose, talvez precise usar elementos de
IU compartilhados no Compose e no sistema de visualização. Por exemplo, caso seu app tenha um
componente CallToActionButton
personalizado, pode ser necessário usá-lo nas telas do Compose
e nas baseadas em visualizações.
No Compose, os elementos de interface compartilhados se tornam elementos combináveis que podem ser reutilizados em
todo o app, não importa se o elemento é estilizado usando XML ou se é uma visualização personalizada. Por
exemplo, você pode criar um elemento combinável CallToActionButton
para o componente Button
de chamada de
ação personalizado.
Para usar o elemento combinável em telas baseadas em visualização, crie um wrapper de visualização personalizado que
se estenda de AbstractComposeView
. No
Content
substituído, coloque o elemento combinável que você criou incluído no seu tema
do Compose, conforme mostrado no exemplo abaixo:
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
Observe que os parâmetros compostos se tornam variáveis mutáveis dentro da visualização
personalizada. Isso torna a visualização personalizada CallToActionViewButton
inflável e utilizável,
como uma visualização tradicional. Confira um exemplo disso na Vinculação de visualizações
abaixo:
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
Se o componente personalizado tiver um estado mutável, consulte a Fonte de verdade do estado.
Priorizar a divisão de estado da apresentação
Tradicionalmente, uma View
tem um estado. Uma View
gerencia campos que
descrevem o que exibir, além de como exibir. Ao
converter uma View
para o Compose, separe os dados renderizados para
alcançar um fluxo de dados unidirecional, conforme explicado em mais detalhes na seção sobre elevação de estado.
Por exemplo, uma View
tem uma propriedade visibility
que descreve se ela está
visível, invisível ou se desapareceu. Essa é uma propriedade inerente da View
. Outras
partes do código podem mudar a visibilidade de uma View
, mas somente a própria View
realmente sabe qual é sua visibilidade atual. A lógica para garantir que
uma View
esteja visível pode ser propensa a erros e geralmente está vinculada à
View
em si.
Por outro lado, o Compose facilita a exibição de elementos que podem ser compostos totalmente diferentes usando a lógica condicional no Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
Por padrão, o CautionIcon
não precisa saber ou se importar por que está sendo mostrado,
e não há o conceito de visibility
: ele está na composição
ou não.
Ao separar de maneira clara a lógica de gerenciamento de estados e apresentação, você pode mudar mais livremente a forma como o conteúdo é exibido como uma conversão de estado para a interface. A elevação do estado quando necessário também torna os elementos combináveis mais reutilizáveis, já que a propriedade do estado é mais flexível.
Promover componentes encapsulados e reutilizáveis
Os elementos da View
geralmente têm uma ideia do local em que residem: dentro de uma Activity
, uma
Dialog
, um Fragment
ou dentro de outra hierarquia de View
. Como eles
geralmente são inflados dos arquivos de layout estático, a estrutura geral de uma
View
tende a ser muito rígida. Isso resulta em um acoplamento rígido e dificulta a
alteração ou reutilização de uma View
.
Por exemplo, uma View
personalizada pode presumir que haja uma visualização filha de determinado
tipo com determinado ID e mudar as propriedades diretamente em resposta a uma
ação. Isso une os elementos da View
de forma rígida: a View
personalizada
poderá falhar ou ser corrompida se não encontrar a filha, que provavelmente não
poderá ser reutilizada sem a View
mãe personalizada.
Isso é um problema menor no Compose com os elementos combináveis reutilizáveis. Os pais podem especificar facilmente o estado e os callbacks para que você possa escrever elementos combináveis reutilizáveis sem precisar saber o local exato em que serão usados.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
No exemplo acima, as três partes estão mais encapsuladas e menos acopladas:
A
ImageWithEnabledOverlay
só precisa saber qual é o estado atual deisEnabled
. Não é necessário saber que oControlPanelWithToggle
existe nem como pode ser controlado.O
ControlPanelWithToggle
não sabe que aImageWithEnabledOverlay
existe. Poderia haver zero, uma ou mais maneiras de exibirisEnabled
, e oControlPanelWithToggle
não precisaria ser mudado.Para o pai, não importa o nível de aninhamento de
ImageWithEnabledOverlay
ouControlPanelWithToggle
. Esses filhos podem animar mudanças, trocar ou transmitir conteúdo para outros filhos.
Esse padrão é conhecido como inversão de controle. Leia mais sobre isso na documentação de CompositionLocal
.
Como gerenciar mudanças no tamanho da tela
Ter recursos diferentes para tamanhos de janela diferentes é uma das principais maneiras de
criar layouts de View
responsivos. Embora os recursos qualificados ainda sejam uma opção
para decisões de layout na tela, o Compose facilita a mudança completa
de layouts em códigos com a lógica condicional normal. Consulte Usar classes de tamanho de
janela para saber mais.
Além disso, consulte Suporte a diferentes tamanhos de tela para saber mais sobre as técnicas que o Compose oferece para a criação de interfaces adaptáveis.
Rolagem aninhada com visualizações
Para ver mais informações sobre como ativar a interoperabilidade de rolagem aninhada entre elementos de visualização e de composição roláveis, aninhados em ambas as direções, consulte Interoperabilidade de rolagem aninhada.
Compose em RecyclerView
Os elementos combináveis em RecyclerView
têm uma boa performance desde a versão RecyclerView
1.3.0-alpha02. Confira se você usa pelo menos a versão 1.3.0-alpha02 da
RecyclerView
para ter esses benefícios.
WindowInsets
interoperabilidade com visualizações
Talvez seja necessário substituir os insets padrão quando a tela tiver visualizações e código do Compose na mesma hierarquia. Nesse caso, é necessário especificar qual deles deve consumir os insetos e qual deve ignorá-los.
Por exemplo, se o layout mais externo for um layout de visualização do Android, consuma os insets no sistema de visualização e ignore-os para o Compose.
Como alternativa, se o layout mais externo for um elemento combinável, consuma as
informações em Compose e adicione os elementos combináveis AndroidView
.
Por padrão, cada ComposeView
consome todos os insets no
nível de consumo WindowInsetsCompat
. Para mudar esse comportamento padrão, defina
ComposeView.consumeWindowInsets
como false
.
Para mais informações, leia a documentação WindowInsets
no Compose.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Mostrar emoji
- Material Design 2 no Compose
- Encartes de janela no Compose