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 da planta 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 do Android.
  • Como usar uma visualização do Android no Compose.
  • Como usar o tema do sistema de visualização no Compose.
  • Como testar uma tela com o código do sistema de visualização e o Compose.

Prerequisites

O que é necessário

2. Planejar a migração

A migração para o Compose depende de você e da sua equipe. Há muitas maneiras diferentes de integrar o Jetpack Compose a um app Android existente. Veja duas estratégias comuns de migração:

  • Desenvolver uma nova tela com o Compose
  • Usar uma tela existente e migrar os componentes dela gradualmente

Compose em novas telas

Uma abordagem comum ao refatorar o app para usar uma nova tecnologia é a adoção dela em novos recursos criados para o app. Neste caso, estamos falando de novas telas. Se você precisar criar uma nova tela da IU no app, use o Compose mesmo que o restante continue a usar o sistema de visualização.

Para isso, faça a interoperabilidade do Compose nas bordas dos recursos migrados.

Compose e visualizações juntos

Você pode migrar algumas partes de uma tela para o Compose e outras para o sistema de visualização. Por exemplo, você pode migrar uma RecyclerView, mas deixar o restante da tela no sistema de visualização.

Ou vice-versa: use o Compose como o layout externo e use algumas visualizações existentes que podem não estar disponíveis no Compose, como MapView ou AdView.

Concluir a migração

Migre fragmentos ou telas inteiras para o Compose, um de cada vez. Mais simples, mas muito generalizado.

E neste 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

Consiga 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:

Abrir o Android Studio

Este codelab exige o Android Studio Bumblebee.

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 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 está disponível em várias ramificações do GitHub:

  • main é a ramificação consultada ou transferida por download. Este é 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 o codelab passo a passo no seu ritmo.

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

Para acessar a ramificação end usando o git, use o seguinte comando:

$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs

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 app/build.gradle (ou build.gradle (Module: compose-migration.app)), vai ver como ele importa as dependências do Compose e permite que o Android Studio funcione com o Compose usando a sinalização buildFeatures { compose true }.

app/build.gradle

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    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.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

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

5. Olá, Compose!

Na tela de detalhes da planta, migraremos a descrição para o Compose, deixando a estrutura geral intacta. Você acompanhará a estratégia de migração do Compose e das visualizações juntos mencionada na seção Planejar a migração.

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 ConstraintLayout e as 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.

plantdetail/PlantDetailDescription.kt

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

Vamos exibir esse texto na tela chamando o elemento que pode ser composto do ComposeView adicionado na etapa anterior. Abra plantdetail/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 o elemento PlantDetailDescription que pode ser composto dentro do MaterialTheme, já que o app Sunflower usa o Material Design.

plantdetail/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 ser exibida 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 PlantName que pode ser composto 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 mapeia para o textAppearanceHeadline5 usando o código XML;
  • os modificadores decoram o elemento "Text" para o ajustar como a versão XML:
  • o modificador fillMaxWidth corresponde ao android:layout_width="match_parent" no código XML;
  • o padding horizontal de margin_small, que é um valor do sistema de visualização, usa a função auxiliar dimensionResource;
  • wrapContentWidth é usado para alinhar o Text horizontalmente.

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 que pode ser composto, use a função LiveData.observeAsState().

Como os valores emitidos pelo LiveData podem ser nulos, você vai precisar agrupar o uso em uma verificação de valores nulos. Por causa disso, e para reutilização, é um bom padrão dividir o consumo de LiveData e o detectar em diferentes elementos que podem ser compostos. Assim, crie um novo elemento que pode ser composto com o nome PlantDetailContent para exibir informações de Plant.

Esta vai ser a aparência do arquivo PlantDetailDescription.kt após a adição da observação do LiveData:

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)
    }
}

Com a mesma visualização de PlantNamePreview, já que, no momento, PlantDetailContent chama PlantName:

3e47e682cf518c71.png

Você conectou o ViewModel por completo para exibir um nome de planta no Compose. Nas próximas seções, você vai criar o restante dos elementos que podem ser compostos 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 a mesma abordagem de antes, você já pode 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}"
    .../>

Como você fez antes, crie um novo elemento que pode ser composto com o nome PlantWatering e adicione Texts para exibir as informações de irrigação na tela:

PlantDetailDescription.kt

@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 = LocalContext.current.resources.getQuantityString(
            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 relevante.

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 usa uma View como parâmetro e fornece um callback para quando a visualização for inflada.

Para isso, vamos criar um novo elemento PlantDescription que pode ser composto. Ele chama a AndroidView com a TextView que acabamos de lembrar em uma lambda. No callback factory, inicialize uma TextView que reaja às interações de HTML usando o Context especificado. E, no callback update, defina o texto com uma descrição formatada em HTML.

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.

Da mesma forma, o callback de atualização AndroidView vai ser recomposto diante de uma mudança de htmlDescription. Qualquer estado lido dentro do callback causa uma recomposição.

Vamos também adicionar uma PlantDescription ao elemento PlantDetailContent que pode ser composto e mudar o código de visualização para exibir 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

Por padrão, o Compose descarta a composição sempre que a ComposeView se desanexa de uma janela. Isso não é desejável quando a ComposeView é usada em fragmentos:

  • 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 e
  • para manter os elementos da IU do Compose na tela quando houver transições neles ou em janelas. Durante as transições, a própria ComposeView permanece visível mesmo depois de ser desanexada da janela.

Você pode chamar o método AbstractComposeView.disposeComposition manualmente para descartar a composição. Para descartar as composições automaticamente quando elas não forem mais necessárias, defina uma estratégia diferente ou crie uma própria chamando o método setViewCompositionStrategy.

Use 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 ver mais informações), e como usaremos tipos de View no Compose, precisamos garantir que a ComposeView use a estratégia DisposeOnViewTreeLifecycleDestroyed. No entanto, é uma prática recomendada definir essa estratégia ao usar a ComposeView em fragmentos.

plantdetail/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 o compose-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.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

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 nos temas claro e escuro:

c99216fc77699dd7.gif

12. Teste

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

A seguir

Confira os outros codelabs no programa de aprendizagem do Compose:

Leia mais