Como migrar para o Jetpack Compose

1. Introdução

O Compose e o sistema de visualização podem trabalhar lado a lado.

Neste codelab, você vai migrar partes da tela de detalhes das plantas do app Sunflower (link em inglês) para o Compose. Criamos uma cópia do projeto para você testar a migração de um app realista para o Compose.

Ao final do codelab, você vai poder continuar a migração e converter o restante das telas do Sunflower, se quiser.

Para receber mais suporte durante este codelab, confira as orientações neste vídeo (em inglês):

O que você vai aprender

Neste codelab, você vai aprender o seguinte:

  • Os diferentes caminhos de migração que você pode seguir.
  • Como migrar um app para o Compose gradualmente.
  • Como adicionar o Compose a uma tela já criada com visualizações.
  • Como usar uma visualização no Compose.
  • Como criar um tema no Compose.
  • Como testar uma tela mista criada com visualizações e com o Compose.

Pré-requisitos

O que é necessário

2. Estratégia de migração

O Jetpack Compose foi desenvolvido com interoperabilidade de visualização desde o início. Recomendamos que a migração para o Compose seja feita de modo incremental, em que esse sistema e as visualizações sejam usados juntos na base de código até que o app passe a usar o Compose totalmente.

A estratégia de migração recomendada é esta:

  1. Criar telas com o Compose.
  2. Ao criar recursos, identifique elementos reutilizáveis e comece a criar uma biblioteca de componentes de interface comuns.
  3. Substituir os recursos atuais uma tela por vez.

Criar telas com o Compose.

Usar o Compose para criar recursos que abrangem uma tela inteira é a melhor maneira de impulsionar a adoção desse kit de ferramentas. Com essa estratégia, você pode adicionar recursos e aproveitar os benefícios do Compose enquanto ainda satisfaz as necessidades de negócios da sua empresa.

Um novo recurso pode abranger uma tela inteira. Nesse caso, toda a tela estaria no Compose. Se você estiver usando a navegação baseada em fragmentos, um novo fragmento será criado e você terá o conteúdo dele no Compose.

Você também pode introduzir novos recursos em uma tela já existente. Nesse caso, as visualizações e o Compose vão coexistir na mesma tela. Por exemplo, digamos que o recurso que você está adicionando seja um novo tipo de visualização em uma RecyclerView. Nesse caso, o novo tipo de visualização ficaria no Compose, e os outros itens permaneceriam os mesmos.

Criar uma biblioteca de componentes de interface comuns

Ao criar recursos com o Compose, você vai acabar criando uma biblioteca de componentes rapidamente. Identifique componentes reutilizáveis e promova a reutilização deles o máximo possível no seu app para que os componentes compartilhados tenham uma única fonte de verdade. Os recursos que você cria podem depender dessa biblioteca.

Substituir recursos existentes pelo Compose

Além de criar novos recursos, você pode migrar gradualmente os recursos atuais do app para o Compose. Você decide como vai fazer isso. Confira algumas boas opções:

  1. Telas simples: telas simples no app e não muito dinâmicas com poucos elementos de interface, por exemplo, a tela de boas-vindas, uma tela de confirmação ou uma tela de configurações. Essas são boas opções para migrar para o Compose, já que é possível fazer isso com apenas algumas linhas de código.
  2. Telas mistas com visualizações e o Compose: telas que já contêm um pouco de código do Compose são outra boa opção, já que você pode continuar a migração dos elementos nessa tela por partes. Se você tem uma tela apenas com uma subárvore no Compose, pode continuar migrando outras partes da árvore até que toda a interface esteja no Compose. Essa é a abordagem de migração de baixo para cima.

Abordagem de baixo para cima da migração de telas que usam visualizações e a interface do Compose para usar apenas o Compose

A abordagem deste codelab

Neste codelab, você vai fazer uma migração gradual para o Compose na tela de detalhes da planta no app Sunflower, trabalhando com o Compose e as visualizações ao mesmo tempo. Depois disso, você poderá continuar a migração por conta própria, se quiser.

3. Etapas da configuração

Buscar o código

Acesse o código do codelab no GitHub:

$ git clone https://github.com/android/codelab-android-compose

Se preferir, faça o download do repositório como um arquivo ZIP:

Como executar o app de exemplo

Você fez o download de um código que contém todos os codelabs disponíveis do Compose. Para concluir este codelab, abra o projeto MigrationCodelab no Android Studio.

Neste codelab, você vai migrar a tela de detalhes da planta do app Sunflower (link em inglês) para o Compose. Você pode abrir a tela de detalhes tocando em uma das plantas disponíveis na lista mostrada no app.

bb6fcf50b2899894.png

Configuração do projeto

O projeto é criado em várias ramificações Git:

  • A ramificação main é o ponto de partida do codelab.
  • end contém a solução deste codelab.

Recomendamos que você comece com o código na ramificação main e siga as etapas do codelab no seu ritmo.

Durante o codelab, você verá snippets de código que precisam ser adicionados ao projeto. Em alguns locais, também vai ser necessário remover o código que é explicitamente mencionado nos comentários dos snippets de código.

Para acessar a ramificação end pelo git, use cd para acessar o diretório do projeto MigrationCodelab e use o comando:

$ git checkout end

Ou faça o download do código da solução aqui:

Perguntas frequentes

4. Compose no app Sunflower

O Compose já foi adicionado ao código que você transferiu por download da ramificação main. No entanto, vamos dar uma olhada no que é necessário para que ele funcione.

Se você abrir o arquivo build.gradle no nível do app, vai notar como ele importa as dependências do Compose e permite que o Android Studio funcione com ele usando a flag buildFeatures { compose true }.

app/build.gradle

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

dependencies {
    //...
    // Compose
    def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"
    //...
}

A versão dessas dependências é definida no arquivo build.gradle do projeto.

5. Olá, Compose!

Na tela de detalhes da planta, migraremos a descrição para o Compose, deixando a estrutura geral intacta.

O Compose precisa de uma atividade ou fragmento do host para renderizar a interface. No Sunflower, como todas as telas usam fragmentos, você vai usar a ComposeView: uma visualização do Android que pode hospedar conteúdo da interface do Compose com o método setContent.

Como remover o código XML

Vamos começar a migração. Abra fragment_plant_detail.xml e faça o seguinte:

  1. Mude para a Visualização "Code".
  2. Remova o código da ConstraintLayout e as quatro TextViews aninhadas na NestedScrollView. O codelab compara e referencia o código XML ao migrar itens individuais. É útil adicionar comentários ao código.
  3. Adicione uma ComposeView para hospedar o código do Compose em vez da compose_view como o ID da visualização.

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <!-- Step 2) Comment out ConstraintLayout and its children ->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    <!-- End Step 2) Comment out until here ->

    <!-- Step 3) Add a ComposeView to host Compose code ->
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Como adicionar código do Compose

Agora, você já pode migrar a tela de detalhes da planta para o Compose.

Ao longo do codelab, você vai adicionar um código do Compose ao arquivo PlantDetailDescription.kt na pasta plantdetail. Abra o arquivo e veja como já existe um texto "Hello Compose" marcador de posição no projeto.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Surface {
        Text("Hello Compose")
    }  
}

Vamos mostrar esse texto na tela chamando a função combinável da ComposeView adicionada na etapa anterior. Abra PlantDetailFragment.kt.

Como a tela está usando a vinculação de dados, você pode acessar diretamente a composeView e chamar setContent para exibir o código do Compose na tela. Chame a função combinável PlantDetailDescription dentro do MaterialTheme, já que o app Sunflower usa o Material Design.

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    // ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            // ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        // ...
    }
}

Se você executar o app, a mensagem "Hello Compose" vai aparecer na tela.

66f3525ecf6669e0.png

6. Como criar um elemento combinável fora do XML

Vamos começar migrando o nome da planta. Mais exatamente, a TextView com o ID @+id/plant_detail_name removido em fragment_plant_detail.xml. Veja o código XML:

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

Veja como a TextView tem um estilo textAppearanceHeadline5, uma margem horizontal de 8.dp e é centralizada horizontalmente na tela. No entanto, o título exibido é observado em um LiveData exposto pelo PlantDetailViewModel da camada do repositório.

Como a observação de um LiveData vai ser abordada mais tarde, vamos supor que o nome esteja disponível e seja transmitido como um parâmetro para um novo elemento combinável PlantName no arquivo PlantDetailDescription.kt. Esse elemento vai ser chamado mais tarde no PlantDetailDescription.

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

Veja uma prévia

d09fe886b98bde91.png

Em que:

  • O estilo de Text é MaterialTheme.typography.headlineSmall, que é semelhante a textAppearanceHeadline5 do código XML.
  • Os modificadores enfeitam o texto para que ele se pareça com a versão do XML:
  • O modificador fillMaxWidth é usado para ocupar o valor máximo de largura disponível. Esse modificador corresponde ao valor de match_parent do atributo layout_width no código XML.
  • O modificador padding é usado para que um valor de padding horizontal de margin_small seja aplicado. Isso corresponde às declarações marginStart e marginEnd no XML. O valor margin_small também é o recurso de dimensão atual que é buscado usando a função auxiliar dimensionResource.
  • O modificador wrapContentWidth é usado para alinhar o texto de modo que ele fique centralizado horizontalmente. Isso é semelhante a uma gravity de center_horizontal no XML.

7. ViewModels e LiveData

Agora, vamos conectar o título à tela. Para fazer isso, carregue os dados usando o PlantDetailViewModel. O Compose vem com integrações para ViewModel e LiveData.

ViewModels

Como uma instância do PlantDetailViewModel é usada no fragmento, ela pode ser transmitida como um parâmetro para PlantDetailDescription.

Abra o arquivo PlantDetailDescription.kt e adicione o parâmetro PlantDetailViewModel a PlantDetailDescription:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    //...
}

Agora, transmita a instância do ViewModel ao chamar esse elemento combinável no fragmento:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

Com isso, você já tem acesso ao campo LiveData<Plant> do PlantDetailViewModel para ver o nome da planta.

Para observar o LiveData em um elemento combinável, use a função LiveData.observeAsState().

Como os valores emitidos pelo LiveData podem ser null, você vai precisar agrupar o uso em uma verificação null. Por isso, e para reutilização, é melhor dividir o consumo de LiveData e detectar em diferentes elementos combináveis. Vamos criar um novo elemento combinável com o nome PlantDetailContent, que vai mostrar informações de Plant.

Com essas atualizações, o arquivo PlantDetailDescription.kt ficará assim:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

A PlantNamePreview precisa refletir nossa mudança sem precisar de uma atualização direta, já que o PlantDetailContent chama PlantName:

3e47e682cf518c71.png

Agora, você conectou o ViewModel para que um nome de planta seja mostrado no Compose. Nas próximas seções, você vai criar o restante dos elementos combináveis e os conectar ao ViewModel de maneira semelhante.

8. Mais migração de código XML

Ficou mais fácil preencher o que falta na nossa interface: as informações de irrigação e a descrição da planta. Seguindo uma abordagem semelhante à anterior, já é possível migrar o restante da tela.

O código XML das informações de irrigação removido de fragment_plant_detail.xml consiste em duas TextViews com IDs plant_watering_header e plant_watering.

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

Assim como você fez antes, crie uma nova função combinável com o nome PlantWatering e adicione elementos combináveis Text para mostrar as informações de irrigação na tela:

PlantDetailDescription.kt

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colorScheme.primaryContainer,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

Veja uma prévia

6f6c17085801a518.png

Algumas coisas a observar:

  • Como o padding horizontal e a decoração do alinhamento são compartilhados pelos elementos Text, você pode atribuir o modificador a uma variável local (por exemplo, centerWithPaddingModifier) para o reutilizar. Isso é possível porque modificadores são objetos normais do Kotlin.
  • O MaterialTheme do Compose não tem uma correspondência exata para o colorAccent usado no plant_watering_header. Por enquanto, use MaterialTheme.colorScheme.primaryContainer. Vamos melhorar isso na seção de aplicação de temas de interoperabilidade.
  • No Compose 1. 2.1 Para usar o pluralStringResource é necessário ativar o ExperimentalComposeUiApi. Em uma versão futura do Compose, talvez isso não seja mais necessário.

Além disso, vamos conectar todas as partes e chamar PlantWatering no PlantDetailContent. O código XML ConstraintLayout que removemos no início tinha uma margem de 16.dp que precisamos incluir no código do Compose.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

Em PlantDetailContent, crie uma Column para mostrar o nome e as informações de irrigação e usar essas informações como padding. Além disso, para que as cores do plano de fundo e do texto sejam adequadas, adicione uma Surface para processar essa informação.

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

Se você atualizar a visualização, vai ver o seguinte:

56626a7118ce075c.png

9. Visualizações no código do Compose

Agora, vamos migrar a descrição da planta. O código em fragment_plant_detail.xml tinha uma TextView com app:renderHtml="@{viewModel.plant.description}" para informar ao XML qual texto mostrar na tela. renderHtml é um adaptador de vinculação que pode ser encontrado no arquivo PlantDetailBindingAdapters.kt. A implementação usa HtmlCompat.fromHtml para definir o texto na TextView.

No entanto, o Compose não oferece suporte a classes Spanned nem à exibição de texto formatado em HTML. Sendo assim, precisamos usar uma TextView do sistema de visualização no código do Compose para ignorar essa limitação.

Como o Compose ainda não pode renderizar o código HTML, você vai criar uma TextView de maneira programática para fazer isso usando a API AndroidView.

A AndroidView permite criar uma View na lambda factory. Ela também fornece uma lambda update, que é invocada quando a visualização é inflada e em recomposições seguintes.

Para isso, vamos criar um novo elemento PlantDescription combinável. Esse elemento combinável chama AndroidView, que constrói uma TextView na lambda factory. Na lambda factory, inicialize uma TextView que mostre texto formatado em HTML e que, em seguida, defina o movementMethod como uma instância de LinkMovementMethod. Por fim, na lambda update, defina o texto da TextView como htmlDescription.

PlantDetailDescription.kt

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Veja uma prévia:

deea1d191e9087b4.png

Observe que a htmlDescription se lembra da descrição em HTML de uma determinada description transmitida como parâmetro. Se o parâmetro description mudar, o código htmlDescription dentro de remember vai ser executado novamente.

Como resultado, o callback de atualização AndroidView vai ser recomposto se o htmlDescription mudar. Qualquer estado lido dentro da lambda update causa uma recomposição.

Vamos também adicionar uma PlantDescription à função combinável PlantDetailContent e mudar o código de visualização para mostrar uma descrição em HTML:

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

Veja uma prévia

7843a8d6c781c244.png

Neste ponto, você migrou todo o conteúdo do ConstraintLayout original para o Compose. Execute o app para ver se ele está funcionando como esperado.

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

O Compose descarta a composição sempre que a ComposeView é removida de uma janela. Isso não é o ideal quando a ComposeView é usada em fragmentos, por dois motivos:

  • A composição precisa seguir o ciclo de vida de visualização do fragmento para que os tipos de View da interface do Compose salvem o estado.
  • Quando ocorrem transições, a ComposeView fica em um estado desconectado. No entanto, os elementos da interface do Compose ainda vão ficar visíveis durante essas transições.

Para modificar esse comportamento, chame setViewCompositionStrategy com a ViewCompositionStrategy adequada para que ela siga o ciclo de vida de visualização do fragmento. Mais especificamente, recomendamos usar a estratégia DisposeOnViewTreeLifecycleDestroyed para descartar a composição quando o LifecycleOwner do fragmento for destruído.

Como o PlantDetailFragment tem transições de entrada e saída (consulte nav_garden.xml para mais informações), e como usaremos tipos de View no Compose, precisamos garantir que a ComposeView use a estratégia DisposeOnViewTreeLifecycleDestroyed. No entanto, a prática recomendada é sempre definir essa estratégia ao usar ComposeView em fragmentos.

PlantDetailFragment.kt

import androidx.compose.ui.platform.ViewCompositionStrategy
...

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.apply {
                // Dispose the Composition when the view's LifecycleOwner
                // is destroyed
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        PlantDetailDescription(plantDetailViewModel)
                    }
                }
            }
        }
        ...
    }
}

11. Temas do Material Design

Migramos o conteúdo de texto dos detalhes da planta para o Compose. No entanto, talvez você tenha percebido que o Compose não está usando as cores certas do tema. O nome da planta, que deveria estar verde, está roxo.

Para usar as cores corretas do tema, é necessário personalizar o MaterialTheme definindo seu próprio tema e fornecendo as cores.

Personalização do MaterialTheme

Para criar seu próprio tema, abra o arquivo Theme.kt no pacote theme. O Theme.kt define um elemento combinável chamado SunflowerTheme, que aceita uma lambda de conteúdo e a transmite para um MaterialTheme.

Ele ainda não faz nada interessante. A personalização será feita a seguir.

Theme.kt

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun SunflowerTheme(
    content: @Composable () -> Unit
) {
    MaterialTheme(content = content)
}

O MaterialTheme permite que você personalize as cores, a tipografia e as formas. Por enquanto, personalize as cores fornecendo as mesmas cores no tema do Sunflower View. O SunflowerTheme também pode aceitar um parâmetro booleano darkTheme, que vai assumir true como padrão se o sistema estiver no modo escuro. Caso contrário, o valor será false. Com esse parâmetro, podemos transmitir os valores de cor corretos ao MaterialTheme para corresponder ao tema do sistema definido no momento.

Theme.kt

@Composable
fun SunflowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val lightColors  = lightColorScheme(
        primary = colorResource(id = R.color.sunflower_green_500),
        primaryContainer = colorResource(id = R.color.sunflower_green_700),
        secondary = colorResource(id = R.color.sunflower_yellow_500),
        background = colorResource(id = R.color.sunflower_green_500),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
    )
    val darkColors  = darkColorScheme(
        primary = colorResource(id = R.color.sunflower_green_100),
        primaryContainer = colorResource(id = R.color.sunflower_green_200),
        secondary = colorResource(id = R.color.sunflower_yellow_300),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
        onBackground = colorResource(id = R.color.sunflower_black),
        surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
        onSurface = colorResource(id = R.color.sunflower_white),
    )
    val colors = if (darkTheme) darkColors else lightColors
    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

Para usar, substitua o MaterialTheme pelo SunflowerTheme. Por exemplo, no PlantDetailFragment:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            SunflowerTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

E todos os elementos da visualização no arquivo PlantDetailDescription.kt:

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    SunflowerTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    SunflowerTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    SunflowerTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Como é possível observar na visualização, as cores agora devem corresponder às do tema Sunflower.

886d7eaea611f4eb.png

Também é possível visualizar a interface no tema escuro criando uma nova função e transmitindo Configuration.UI_MODE_NIGHT_YES ao uiMode:

import android.content.res.Configuration
...

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

Veja uma prévia

cfe11c109ff19eeb.png

Se você executar o app, ele vai se comportar da mesma forma que antes da migração, tanto no tema claro quanto no escuro:

438d2dd9f8acac39.gif

12. Testes

Depois de migrar partes da tela de detalhes da planta para o Compose, é fundamental fazer os testes para garantir que tudo esteja funcionando bem.

No app Sunflower, o PlantDetailFragmentTest localizado na pasta androidTest testa algumas funcionalidades do app. Abra o arquivo e dê uma olhada no código atual:

  • testPlantName verifica o nome da planta na tela.
  • testShareTextIntent verifica se a intent correta é acionada após um toque no botão "Share" (compartilhar).

Quando uma atividade ou um fragmento usa o Compose, em vez de usar a ActivityScenarioRule, você precisa usar a createAndroidComposeRule, que integra a ActivityScenarioRule com uma ComposeTestRule que possibilita testar o código do Compose.

Em PlantDetailFragmentTest, substitua o uso de ActivityScenarioRule por createAndroidComposeRule. Quando a regra de atividade for necessária para configurar o teste, use o atributo activityRule da createAndroidComposeRule desta maneira:

@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {

    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()
   
    ...

    @Before
    fun jumpToPlantDetailFragment() {
        populateDatabase()

        composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }
    }

    ...
}

Se você executar os testes, o testPlantName vai falhar. A função testPlantName verifica se há uma TextView na tela. No entanto, você migrou essa parte da interface para o Compose. Então, é necessário usar as declarações do Compose:

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

Se você executar os testes, vai ver que todos são aprovados.

dd59138fac1740e4.png

13. Parabéns

Parabéns, você concluiu este codelab.

A ramificação compose (link em inglês) do projeto original do app Sunflower no GitHub migra completamente a tela de detalhes da planta para o Compose. Além do que você fez neste codelab, ela também simula o comportamento do CollapsingToolbarLayout. Isso envolve:

  • Carregar imagens com o Compose
  • Animações
  • Processamento de dimensões aprimorado
  • E muito mais

Qual é a próxima etapa?

Confira os outros codelabs no programa de aprendizagem do Compose:

Leia mais