Como integrar o Compose à IU já existente

Se você tiver um app que usa uma IU com base em visualização, talvez não queira reescrever toda a IU de uma vez só. Esta página ajudará você a adicionar novos elementos do Compose à sua IU já existente.

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 IU compartilhados se tornam elementos compostos que podem ser reutilizados em todo o app, independentemente de o elemento ser estilizado usando XML ou ser uma visualização personalizada. Por exemplo, você pode criar um elemento CallToActionButton composto para o componente Button de chamada para ação personalizado.

Para usar esse elemento em telas baseadas em visualização, é necessário criar um wrapper de visualização personalizado que se estenda de AbstractComposeView. No Content substituído, coloque o elemento composto 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(
            backgroundColor = MaterialTheme.colors.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<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @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, por exemplo, com vinculação de visualizações, assim como uma visualização tradicional. Veja o exemplo abaixo:

class ExampleActivity : Activity() {

    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.something)
            onClick = { /* Do something */ }
        }
    }
}

Se o componente personalizado tiver um estado mutável, consulte a Fonte de verdade do estado.

Temas

Seguindo o Material Design, o uso da biblioteca Material Design Components for Android (MDC) é a maneira recomendada de aplicar temas a apps Android. Conforme abordado na documentação de temas no Compose, o Compose implementa esses conceitos com a função composta MaterialTheme.

Ao criar novas telas no Compose, aplique um MaterialTheme antes de elementos compostos que emitem a IU da biblioteca Material Design Components. Os componentes do Material Design (Button, Text etc.) dependem da existência de um MaterialTheme, e o comportamento deles fica indefinido sem isso.

Todas as amostras do Jetpack Compose usam um tema personalizado do Compose criado sobre MaterialTheme.

Várias fontes da verdade

Um app provavelmente tem uma grande quantidade de temas e estilos para visualizações. Quando você introduz o Compose em um app existente, é necessário migrar o tema para usar MaterialTheme em qualquer tela do Compose. Isso significa que os temas do seu app terão duas fontes da verdade: o tema baseado na visualização e o tema do Compose. Qualquer mudança no estilo precisa ser feita em vários lugares.

Se você pretende migrar o app totalmente para o Compose, é preciso criar uma versão do tema existente para ele. O problema é que, quanto antes no processo de desenvolvimento você criar o tema do Compose, mais manutenção você precisará fazer durante o desenvolvimento.

Biblioteca MDC Compose Theme Adapter

Se estiver usando a biblioteca MDC no seu app Android, a biblioteca MDC Compose Theme Adapter permitirá que você reutilize facilmente nas suas funções compostas os temas de cor, tipografia e forma presentes nos temas baseados na visualização existentes:

import com.google.android.material.composethemeadapter.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Consulte a documentação da biblioteca MDC para saber mais.

Biblioteca AppCompat Compose Theme Adapter

A biblioteca AppCompat Compose Theme Adapter (link em inglês) permite reutilizar facilmente temas XML da AppCompat para definir temas no Jetpack Compose. Ela cria um MaterialTheme com os valores de cor e tipografia (links em inglês) usando o tema do contexto.

import com.google.accompanist.appcompattheme.AppCompatTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Estilos padrão dos componentes

As bibliotecas MDC e AppCompat Compose Theme Adapter não leem nenhum estilo de widget padrão definido pelo tema. Isso ocorre porque o Compose não tem o conceito de funções default compostas.

Saiba mais sobre estilos de componentes e sistemas de design personalizados na documentação de temas.

Sobreposições de tema no Compose

Ao migrar telas baseadas em visualização para o Compose, preste atenção aos usos do atributo android:theme. É provável que você precise de um novo MaterialTheme nessa parte da árvore de IU do Compose.

Leia mais sobre isso no Guia de temas.

Animações WindowInsets e IME

Você pode processar WindowInsets usando a biblioteca accompanist-insets, que fornece elementos que podem ser compostos e modificadores para gerenciá-los nos seus layouts, além de compatibilidade com animações do IME.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Animação mostrando um elemento da IU rolando para cima e para baixo para dar lugar ao teclado.

Figura 2. Animações do IME usando a biblioteca accompanist-insets.

Consulte a documentação da biblioteca accompanists-insets para saber mais.

Como gerenciar mudanças no tamanho da tela

Ao migrar um app que usa layouts XML diferentes, dependendo do tamanho da tela, use o elemento BoxWithConstraints para saber o tamanho mínimo e máximo que um composto pode ocupar.

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

Rolagem aninhada com visualizações

Infelizmente, a rolagem aninhada entre o sistema de visualização e o Jetpack Compose ainda não está disponível. É possível verificar o andamento neste bug do Issue Tracker.

Compose na RecyclerView

O Jetpack Compose usa o DisposeOnDetachedFromWindow como ViewCompositionStrategy padrão. Isso significa que a composição é descartada sempre que a visualização é removida da janela.

Ao usar uma ComposeView como parte de um armazenador de visualização da RecyclerView, a estratégia padrão é ineficiente, porque as instâncias de composição permanecerão na memória até a RecyclerView ser removida da janela. O descarte da composição quando a ComposeView não é mais necessária para a RecyclerView é uma prática recomendada.

A função disposeComposition possibilita descartar manualmente a composição de uma ComposeView. Você pode chamar essa função quando a visualização é reciclada desta forma:

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

Para fazer o armazenador de visualização do Compose funcionar em todos os cenários, é necessário usar a estratégia DisposeOnViewTreeLifecycleDestroyed, conforme abordado na seção ViewCompositionStrategy para ComposeView do Guia de APIs de interoperabilidade.

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

Para ver a ComposeView usada na RecyclerView em ação, confira a ramificação compose_recyclerview do app Sunflower.