Outras considerações

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:

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.

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 de isEnabled. Não é necessário saber que o ControlPanelWithToggle existe nem como pode ser controlado.

  • O ControlPanelWithToggle não sabe que a ImageWithEnabledOverlay existe. Poderia haver zero, uma ou mais maneiras de exibir isEnabled, e o ControlPanelWithToggle não precisaria ser mudado.

  • Para o pai, não importa o nível de aninhamento de ImageWithEnabledOverlay ou ControlPanelWithToggle. 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 tamanhos de tela diferentes para saber mais sobre as técnicas que o Compose oferece para criar 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.

Interoperabilidade de WindowInsets com visualizações

Pode ser necessário substituir os encartes padrão quando a tela tiver o código das visualizações e 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.