Como migrar para o Jetpack Compose

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

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 usar um tema baseado em visualização no Compose.
  • Como testar uma tela mista criada com visualizações e 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 novos recursos com o Compose.
  2. Ao criar recursos, identifique elementos reutilizáveis e comece a criar uma biblioteca de componentes de IU comuns.
  3. Substituir os recursos atuais uma tela por vez.

Criar novos recursos com o Compose

Usar o Compose para criar novos recursos é a melhor maneira de impulsionar a adoção desse sistema. Dessa forma, os novos recursos adicionados podem aproveitar os benefícios do Compose.

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, você criaria um novo fragmento e teria o conteúdo dele no Compose.

Por outro lado, se o novo recurso que você está criando faz parte de uma tela já existente, 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 IU 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 IU, 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 IU 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 IU 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/googlecodelabs/android-compose-codelabs

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 rootProject.composeVersion
    }
}

dependencies {
    //...
    // Compose
    implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
    implementation "androidx.compose.material:material:$rootProject.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
    implementation "com.google.accompanist:accompanist-themeadapter-material:$rootProject.accompanistVersion"
    //...
}

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 IU. No Sunflower, como todas as telas usam fragmentos, você vai usar a ComposeView: uma visualização do Android que pode hospedar conteúdo da IU 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 de composição 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 de composição 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 que pode ser composto 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 de composição 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.h5,
        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.h5, 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 que pode ser composto 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 IU: 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.colors.primaryVariant,
            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.colors.primaryVariant. 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 que você construa uma View na lamba 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 IU do Compose salvem o estado.
  • Quando ocorrem transições, a ComposeView fica em um estado desconectado. No entanto, os elementos da IU 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. Aplicação de temas de interoperabilidade

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.

Neste estado inicial da migração, talvez você queira que o Compose herde os temas disponíveis no sistema de visualização em vez de reescrever do zero um tema próprio do Material Design. Os temas do Material Design funcionam perfeitamente com todos os componentes do Material Design no Compose.

Para reutilizar o tema Material Design Components (MDC) do sistema de visualização no Compose, use a biblioteca Accompanist Material Theme Adapter (link em inglês). A função MdcTheme vai ler automaticamente o tema MDC do contexto do host e o transmitirá para o MaterialTheme por você, tanto para temas claros quanto escuros. Embora você precise apenas das cores do tema neste codelab, a biblioteca também lê as formas e a tipografia do sistema de visualização.

A biblioteca já está incluída no arquivo app/build.gradle:

...
dependencies {
    ...
    implementation "com.google.accompanist:accompanist-themeadapter-material:$rootProject.accompanistVersion"
    ...
}

Para usá-la, substitua os usos de MaterialTheme por MdcTheme. Por exemplo, em PlantDetailFragment:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            MdcTheme {
                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, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

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

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

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

Como é possível ver na visualização, o MdcTheme está recebendo as cores do tema no arquivo styles.xml.

886d7eaea611f4eb.png

Também é possível visualizar a IU 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, "")
    MdcTheme {
        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:

c99216fc77699dd7.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 IU 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