Interoperabilidade de visualização no Compose

1. Antes de começar

Introdução

Neste ponto do curso, você já sabe criar apps com o Compose e tem conhecimento para fazer essa criação com XML, visualizações, vinculações de visualização e fragmentos. Depois de criar apps com visualizações, talvez você comece a gostar da conveniência de uma interface declarativa, como o Compose. No entanto, há alguns casos em que faz sentido usar visualizações em vez do Compose. Neste codelab, você vai aprender a usar as interoperabilidades de visualização para adicionar componentes de visualização a um app moderno do Compose.

No momento da elaboração deste codelab, os componentes da interface que você quer criar ainda não estão disponíveis no Compose. Esta é a oportunidade perfeita para usar a interoperabilidade de visualização.

Pré-requisitos:

O que é necessário

  • Um computador com acesso à Internet e o Android Studio.
  • Um dispositivo ou emulador.
  • O código inicial do app Juice Tracker.

O que você vai criar

Neste codelab, você vai integrar três visualizações à interface do Compose para concluir a interface do app Juice Tracker: uma Spinner, uma RatingBar e uma AdView. Para criar esses componentes, use a interoperabilidade de visualização. Com ela, você pode adicionar visualizações ao seu app envolvendo-as em um elemento combinável.

a02177f6b6277edc.png afc4551fde8c3113.png 5dab7f58a3649c04.png

Tutorial do código

Neste codelab, você vai trabalhar com o mesmo app Juice Tracker dos codelabs Criar um app Android com visualizações e Adicionar o Compose a um app baseado em visualização. A diferença desta versão é que o código inicial fornecido está todo no Compose. No momento, o app não tem as entradas de cor e nota na página da caixa de diálogo de registro e no banner de anúncio na parte de cima da tela da lista.

O diretório bottomsheet contém todos os componentes da interface relacionados à caixa de diálogo de registro. Esse pacote precisa conter os componentes da interface para as entradas de cor e nota, quando criados.

A homescreen contém os componentes da interface hospedados pela tela inicial, que inclui a lista JuiceTracker. Esse pacote precisa conter o banner de anúncio, quando criado.

Os principais componentes da interface, como a página inferior e a lista de sucos, estão hospedados no arquivo JuiceTrackerApp.kt.

2. Acessar o código inicial

Para começar, faça o download do código inicial:

Outra opção é clonar o repositório do GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-starter
  1. No Android Studio, abra a pasta basic-android-kotlin-compose-training-juice-tracker.
  2. Abra o código do app Juice Tracker no Android Studio.

3. Configuração do Gradle

Adicione a dependência de anúncios do Google Play Services ao arquivo build.gradle.kts do app.

app/build.gradle.kts

android {
   ...
   dependencies {
      ...
      implementation("com.google.android.gms:play-services-ads:22.2.0")
   }
}

4. Configurar

Adicione o seguinte valor ao manifesto do Android, acima da tag activity, para ativar o banner de anúncio para testes:

AndroidManifest.xml

...
<meta-data
   android:name="com.google.android.gms.ads.APPLICATION_ID"
   android:value="ca-app-pub-3940256099942544~3347511713" />

...

5. Preencher a caixa de diálogo de registro

Nesta seção, você vai preencher a caixa de diálogo de registro criando o ícone de carregamento de cores e a barra de nota. O ícone de carregamento de cores é o componente que permite escolher uma cor, e a barra de nota permite dar uma nota para o suco. Confira o design abaixo:

Ícone de carregamento de cores com várias cores listadas

Barra de nota com 4 de 5 estrelas selecionadas

Criar o ícone de carregamento de cores

Para implementar um ícone de carregamento no Compose, temos que usar a classe Spinner. A Spinner é um componente de visualização, e não um elemento combinável. Por isso, a implementação precisa usar interoperabilidade.

  1. No diretório bottomsheet, crie um arquivo com o nome ColorSpinnerRow.kt.
  2. Crie uma classe dentro do arquivo chamado SpinnerAdapter.
  3. No construtor do SpinnerAdapter, defina um parâmetro de callback com o nome onColorChange que usa um parâmetro Int. O SpinnerAdapter processa as funções de callback do Spinner.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
  1. Implemente a interface AdapterView.OnItemSelectedListener.

A implementação dessa interface permite definir o comportamento de clique do ícone de carregamento. Você vai configurar esse adaptador em um elemento combinável mais tarde.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
}
  1. Implemente as funções de membro AdapterView.OnItemSelectedListener: onItemSelected() e onNothingSelected().

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        TODO("Not yet implemented")
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. Modifique a função onItemSelected() para chamar a função de callback onColorChange(). Assim, quando você selecionar uma cor, o app vai atualizar o valor selecionado na interface.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. Modifique a função onNothingSelected() para definir a cor como 0. Assim, quando você não selecionar nada, a cor padrão será a primeira, vermelha.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        onColorChange(0)
    }
}

O SpinnerAdapter, que define o comportamento do ícone de carregamento com as funções de callback, já foi criado. Agora, você precisa criar o conteúdo do ícone de carregamento e preenchê-lo com dados.

  1. Dentro do arquivo ColorSpinnerRow.kt, mas fora da classe SpinnerAdapter, crie um elemento combinável chamado ColorSpinnerRow.
  2. Na assinatura do método de ColorSpinnerRow(), adicione um parâmetro Int para a posição do ícone de carregamento, uma função de callback que usa um parâmetro Int e um modificador.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
}
  1. Dentro da função, crie uma matriz dos recursos de string de cores dos sucos usando o tipo enumerado JuiceColor. Essa matriz serve como o conteúdo que vai preencher o ícone de carregamento.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }

}
  1. Adicione um elemento combinável InputRow() e transmita o recurso de string de cores para o rótulo de entrada e um modificador, que define a linha de entrada em que o Spinner aparece.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
   }
}

Em seguida, crie o Spinner. Como o Spinner é uma classe de visualização, a API de interoperabilidade de visualização do Compose precisa ser usada para o envolver em um elemento combinável. Isso pode ser feito com o combinável AndroidView.

  1. Para usar um Spinner no Compose, crie um elemento combinável AndroidView() no corpo da lambda InputRow. O AndroidView() cria um elemento ou uma hierarquia de visualização em um combinável.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
      AndroidView()
   }
}

O elemento combinável AndroidView usa três parâmetros:

  • A lambda factory, que é uma função que cria a visualização.
  • O callback update, que é chamado quando a visualização criada na factory é inflada.
  • Um elemento combinável modifier.

3bb9f605719b173.png

  1. Para implementar o AndroidView, comece transmitindo um modificador e preenchendo a largura máxima da tela.
  2. Transmita uma lambda para o parâmetro factory.
  3. A lambda factory usa um Context como parâmetro. Crie uma classe Spinner e transmita o contexto.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         modifier = Modifier.fillMaxWidth(),
         factory = { context ->
            Spinner(context)
         }
      )
   }
}

Assim como um RecyclerView.Adapter fornece dados a um RecyclerView, um ArrayAdapter fornece dados a um Spinner. O Spinner exige um adaptador para armazenar a matriz de cores.

  1. Defina o adaptador usando um ArrayAdapter. O ArrayAdapter exige um contexto, um layout XML e uma matriz. Transmita simple_spinner_dropdown_item para o layout, que é fornecido como padrão com o Android.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         ​​modifier = Modifier.fillMaxWidth(),
         factory = { context ->
             Spinner(context).apply {
                 adapter =
                     ArrayAdapter(
                         context,
                         android.R.layout.simple_spinner_dropdown_item,
                         juiceColorArray
                     )
             }
         }
      )
   }
}

O callback factory retorna uma instância da visualização criada dentro dele. O update é um callback que usa um parâmetro do mesmo tipo retornado pelo callback factory. Esse parâmetro é uma instância da visualização inflada por factory. Nesse caso, como um Spinner foi criado na fábrica, a instância desse Spinner pode ser acessada no corpo da lambda update.

  1. Adicione um callback update que transmita um spinner. Use o callback fornecido em update para chamar o método setSelection().

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      //...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}
  1. Use o SpinnerAdapter criado anteriormente para definir um callback onItemSelectedListener() no update.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         // ...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}

O código para o componente do ícone de carregamento de cores foi concluído.

  1. Adicione a seguinte função utilitária para conferir o índice de tipo enumerado de JuiceColor. Você usará isso na próxima etapa.
private fun findColorIndex(color: String): Int {
   val juiceColor = JuiceColor.valueOf(color)
   return JuiceColor.values().indexOf(juiceColor)
}
  1. Implemente ColorSpinnerRow no elemento combinável SheetForm no arquivo EntryBottomSheet.kt. Coloque o ícone de carregamento de cores após o texto "Description" e acima dos botões.

bottomsheet/EntryBottomSheet.kt

...
@Composable
fun SheetForm(
   juice: Juice,
   onUpdateJuice: (Juice) -> Unit,
   onCancel: () -> Unit,
   onSubmit: () -> Unit,
   modifier: Modifier = Modifier,
) {
   ...
   TextInputRow(
            inputLabel = stringResource(R.string.juice_description),
            fieldValue = juice.description,
            onValueChange = { description -> onUpdateJuice(juice.copy(description = description)) },
            modifier = Modifier.fillMaxWidth()
        )
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
   ButtonRow(
            modifier = Modifier
                .align(Alignment.End)
                .padding(bottom = dimensionResource(R.dimen.padding_medium)),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

Criar a entrada de nota

  1. Crie um arquivo com o nome RatingInputRow.kt no diretório bottomsheet.
  2. No arquivo RatingInputRow.kt, crie um elemento combinável com o nome RatingInputRow().
  3. Na assinatura do método, transmita um Int para a nota, um callback com um parâmetro Int para processar uma mudança de seleção e um modificador.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
  1. Como na ColorSpinnerRow, adicione uma InputRow ao elemento combinável que contém um AndroidView, conforme mostrado no código de exemplo a seguir.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = {},
            update = {}
        )
    }
}
  1. No corpo da lambda factory, crie uma instância da classe RatingBar, que fornece o tipo de barra de nota necessária para este design. Defina o stepSize como 1f para que a nota sempre seja um número inteiro.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = {}
        )
    }
}

Quando a visualização é inflada, a nota é definida. Não se esqueça que factory retorna a instância de RatingBar ao callback de atualização.

  1. Use a nota transmitida ao elemento combinável para definir a nota da instância RatingBar no corpo da lambda update.
  2. Quando uma nova nota for definida, use o callback RatingBar para chamar a função de callback onRatingChange() e atualizar a nota na interface.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = { ratingBar ->
                ratingBar.rating = rating.toFloat()
                ratingBar.setOnRatingBarChangeListener { _, _, _ ->
                    onRatingChange(ratingBar.rating.toInt())
                }
            }
        )
    }
}

O elemento combinável de entrada de nota foi concluído.

  1. Use o elemento combinável RatingInputRow() no EntryBottomSheet. Posicione-o depois do ícone de carregamento de cores e acima dos botões.

bottomsheet/EntryBottomSheet.kt

@Composable
fun SheetForm(
    juice: Juice,
    onUpdateJuice: (Juice) -> Unit,
    onCancel: () -> Unit,
    onSubmit: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        ...
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
        RatingInputRow(
            rating = juice.rating,
            onRatingChange = { rating -> onUpdateJuice(juice.copy(rating = rating)) }
        )
        ButtonRow(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

Criar o banner de anúncio

  1. No pacote homescreen, crie um arquivo com o nome AdBanner.kt.
  2. No arquivo AdBanner.kt, crie um elemento combinável chamado AdBanner().

Ao contrário dos elementos combináveis anteriores, o AdBanner não exige uma entrada. Não é necessário envolver essa função em um elemento combinável InputRow. No entanto, ela exige uma AndroidView.

  1. Tente criar o banner por conta própria usando a classe AdView. Defina o tamanho do anúncio como AdSize.BANNER e o ID do bloco de anúncios como "ca-app-pub-3940256099942544/6300978111".
  2. Quando a AdView for inflada, carregue um anúncio usando a AdRequest Builder.

homescreen/AdBanner.kt

@Composable
fun AdBanner(modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            AdView(context).apply {
                setAdSize(AdSize.BANNER)
                // Use test ad unit ID
                adUnitId = "ca-app-pub-3940256099942544/6300978111"
            }
        },
        update = { adView ->
            adView.loadAd(AdRequest.Builder().build())
        }
    )
}
  1. Coloque o AdBanner antes da JuiceTrackerList no JuiceTrackerApp. A JuiceTrackerList é declarada na linha 83.

ui/JuiceTrackerApp.kt

...
AdBanner(
   Modifier
       .fillMaxWidth()
       .padding(
           top = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_small)
       )
)

JuiceTrackerList(
    juices = trackerState,
    onDelete = { juice -> juiceTrackerViewModel.deleteJuice(juice) },
    onUpdate = { juice ->
        juiceTrackerViewModel.updateCurrentJuice(juice)
        scope.launch {
            bottomSheetScaffoldState.bottomSheetState.expand()
        }
     },
)

6. Acessar o código da solução

Para fazer o download do código do codelab concluído, use estes comandos git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-with-views

Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Se você quiser ver o código da solução, acesse o GitHub (link em inglês).

7. Saiba mais

8. Conseguimos!

O curso pode terminar aqui, mas este é apenas o começo da sua jornada de desenvolvimento de apps Android.

Neste curso, você aprendeu a criar apps usando o Jetpack Compose, um kit de ferramentas de interface moderno para criar apps Android nativos. Além disso, você criou apps com listas, uma ou várias telas e navegou entre elas. Você aprendeu a criar apps interativos, fez o app responder à entrada do usuário e atualizou a interface. Você aplicou o Material Design e usou cores, formas e tipografia no tema do app. Você também usou o Jetpack e outras bibliotecas de terceiros para agendar tarefas, extrair dados de servidores remotos, manter dados localmente e muito mais.

Ao concluir este curso, você não só entende muito bem como criar apps lindos e responsivos usando o Jetpack Compose, como também tem o conhecimento e as habilidades necessárias para criar apps Android eficientes, fáceis de manter e visualmente atrativos. Essa base vai ajudar você a continuar aprendendo e desenvolvendo suas habilidades no Modern Android Development e no Compose.

Agradecemos a todos pela participação e conclusão do curso. Incentivamos que continuem aprendendo e ampliando suas habilidades com outros recursos, como os documentos da página Desenvolvedores Android, o curso Jetpack Compose para desenvolvedores Android, a documentação Arquitetura moderna de apps Android, o Blog de desenvolvedores Android (link em inglês), outros codelabs e projetos de exemplo (link em inglês).

Por fim, não se esqueça de compartilhar nas mídias sociais o que você criou e usar a hashtag #AndroidBasics para que nós e o restante da comunidade de desenvolvedores Android também possamos acompanhar sua jornada de aprendizado.

Divirta-se!