Este codelab faz parte do curso Conceitos básicos do Kotlin para Android. Você vai aproveitar mais este curso se fizer os codelabs em sequência. Todos os codelabs do curso estão listados na página inicial dos codelabs de princípios básicos do Kotlin para Android.
Introdução
No codelab anterior, você atualizou o app TrackMySleepQuality para mostrar dados sobre a qualidade do sono em um RecyclerView. As técnicas que você aprendeu ao criar seu primeiro RecyclerView são suficientes para a maioria dos RecyclerViews que mostram listas simples que não são muito grandes. No entanto, há várias técnicas que tornam o RecyclerView mais eficiente para listas grandes e facilitam a manutenção e extensão do código para listas e grades complexas.
Neste codelab, você vai criar o app de rastreamento do sono do codelab anterior. Você vai aprender uma maneira mais eficaz de atualizar a lista de dados de sono e como usar a vinculação de dados com RecyclerView. Se você não tiver o app do codelab anterior, faça o download do código inicial para este codelab.
O que você já precisa saber
- Criar uma interface básica do usuário usando uma atividade, fragmentos e visualizações.
- Navegar entre fragmentos e usar
safeArgspara transmitir dados entre eles. - Ver modelos, fábricas de modelos, transformações,
LiveDatae os observadores deles. - Como criar um banco de dados
Room, um DAO e definir entidades. - Como usar corrotinas para banco de dados e outras tarefas de longa duração.
- Como implementar um
RecyclerViewbásico com umAdapter,ViewHoldere um layout de item.
O que você vai aprender
- Como usar
DiffUtilpara atualizar de forma eficiente uma lista exibida porRecyclerView. - Como usar a vinculação de dados com
RecyclerView. - Como usar adaptadores de vinculação para transformar dados.
Atividades deste laboratório
- Crie com base no app TrackMySleepQuality do codelab anterior desta série.
- Atualize o
SleepNightAdapterpara atualizar a lista de maneira eficiente usandoDiffUtil. - Implemente a vinculação de dados para o
RecyclerViewusando adaptadores de vinculação para transformar os dados.
O app de monitoramento do sono tem duas telas, representadas por fragmentos, conforme mostrado na figura abaixo.
|
|
A primeira tela, mostrada à esquerda, tem botões para iniciar e interromper o rastreamento. A tela mostra alguns dos dados de sono do usuário. O botão Limpar exclui permanentemente todos os dados que o app coletou do usuário. A segunda tela, mostrada à direita, é para selecionar uma classificação de qualidade do sono.
Esse app foi arquitetado para usar um controlador de UI, ViewModel e LiveData, e um banco de dados Room para manter os dados de sono.

Os dados de sono são mostrados em um RecyclerView. Neste codelab, você vai criar a parte de vinculação de dados e DiffUtil para o RecyclerView. Depois deste codelab, seu app vai ter a mesma aparência, mas será mais eficiente e fácil de escalonar e manter.
Você pode continuar usando o app SleepTracker do codelab anterior ou fazer o download do app RecyclerViewDiffUtilDataBinding-Starter no GitHub.
- Se necessário, faça o download do app RecyclerViewDiffUtilDataBinding-Starter (link em inglês) no GitHub e abra o projeto no Android Studio.
- Execute o app.
- Abra o arquivo
SleepNightAdapter.kt. - Inspecione o código para se familiarizar com a estrutura do app. Consulte o diagrama abaixo para recapitular o uso de
RecyclerViewcom o padrão de adaptador para mostrar dados de sono ao usuário.

- Com base na entrada do usuário, o app cria uma lista de objetos
SleepNight. Cada objetoSleepNightrepresenta uma única noite de sono, a duração e a qualidade dela. - O
SleepNightAdapteradapta a lista de objetosSleepNightpara algo que oRecyclerViewpossa usar e mostrar. - O adaptador
SleepNightAdapterproduzViewHoldersque contêm as visualizações, os dados e as metainformações para que a visualização de reciclagem mostre os dados. - O
RecyclerViewusa oSleepNightAdapterpara determinar quantos itens serão mostrados (getItemCount()). ORecyclerViewusaonCreateViewHolder()eonBindViewHolder()para receber os titulares de visualização vinculados aos dados para exibição.
O método notifyDataSetChanged() é ineficiente
Para informar ao RecyclerView que um item na lista mudou e precisa ser atualizado, o código atual chama notifyDataSetChanged() no SleepNightAdapter, conforme mostrado abaixo.
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}No entanto, notifyDataSetChanged() informa a RecyclerView que toda a lista pode ser inválida. Como resultado, o RecyclerView faz uma nova vinculação e redesenha todos os itens da lista, incluindo aqueles que não estão visíveis na tela. Isso é muito trabalho desnecessário. Para listas grandes ou complexas, esse processo pode levar tempo suficiente para que a tela pisque ou gagueje enquanto o usuário rola a lista.
Para corrigir esse problema, diga ao RecyclerView exatamente o que mudou. Em seguida, o RecyclerView pode atualizar apenas as visualizações que mudaram na tela.
O RecyclerView tem uma API avançada para atualizar um único elemento. Você pode usar notifyItemChanged() para informar ao RecyclerView que um item mudou, e funções semelhantes para itens adicionados, removidos ou movidos. Você pode fazer tudo manualmente, mas essa tarefa não é trivial e pode envolver um pouco de código.
Felizmente, existe uma maneira melhor.
O DiffUtil é eficiente e faz o trabalho pesado para você
O RecyclerView tem uma classe chamada DiffUtil, que calcula as diferenças entre duas listas. O DiffUtil usa uma lista antiga e uma nova para descobrir o que é diferente. Ele encontra itens que foram adicionados, removidos ou alterados. Em seguida, ele usa um algoritmo chamado Eugene W. Myers para descobrir o número mínimo de mudanças necessárias para transformar a lista antiga na nova.
Depois que o DiffUtil descobre o que mudou, o RecyclerView pode usar essas informações para atualizar apenas os itens que foram alterados, adicionados, removidos ou movidos, o que é muito mais eficiente do que refazer a lista inteira.
Nesta tarefa, você vai fazer upgrade do SleepNightAdapter para usar DiffUtil e otimizar o RecyclerView para mudanças nos dados.
Etapa 1: implementar SleepNightDiffCallback
Para usar a funcionalidade da classe DiffUtil, estenda DiffUtil.ItemCallback.
- Abra
SleepNightAdapter.kt. - Abaixo da definição completa da classe
SleepNightAdapter, crie uma nova classe de nível superior chamadaSleepNightDiffCallbackque estendeDiffUtil.ItemCallback. TransmitaSleepNightcomo um parâmetro genérico.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}- Coloque o cursor no nome da classe
SleepNightDiffCallback. - Pressione
Alt+Enter(Option+Enterno Mac) e selecione Implementar membros. - Na caixa de diálogo exibida, pressione Shift e clique com o botão esquerdo do mouse para selecionar os métodos
areItemsTheSame()eareContentsTheSame(). Em seguida, clique em OK.
Isso gera stubs emSleepNightDiffCallbackpara os dois métodos, como mostrado abaixo. ODiffUtilusa esses dois métodos para descobrir como a lista e os itens mudaram.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}- Em
areItemsTheSame(), substitua oTODOpor um código que testa se os dois itensSleepNighttransmitidos,oldItemenewItem, são iguais. Se os itens tiverem o mesmonightId, eles serão iguais. Portanto, retornetrue. Caso contrário, retornefalse. ODiffUtilusa esse teste para ajudar a descobrir se um item foi adicionado, removido ou movido.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem.nightId == newItem.nightId
}- Em
areContentsTheSame(), verifique seoldItemenewItemcontêm os mesmos dados, ou seja, se são iguais. Essa verificação de igualdade vai verificar todos os campos, porqueSleepNighté uma classe de dados. As classesDatadefinem automaticamenteequalse alguns outros métodos para você. Se houver diferenças entreoldItemenewItem, esse código vai informar aDiffUtilque o item foi atualizado.
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem == newItem
}É comum usar um RecyclerView para mostrar uma lista que muda. O RecyclerView fornece uma classe de adaptador, ListAdapter, que ajuda a criar um adaptador RecyclerView com suporte de uma lista.
O ListAdapter acompanha a lista e notifica o adaptador quando ela é atualizada.
Etapa 1: mudar o adaptador para estender ListAdapter
- No arquivo
SleepNightAdapter.kt, mude a assinatura da classeSleepNightAdapterpara estenderListAdapter. - Se solicitado, importe
androidx.recyclerview.widget.ListAdapter. - Adicione
SleepNightcomo o primeiro argumento aoListAdapter, antes deSleepNightAdapter.ViewHolder. - Adicione
SleepNightDiffCallback()como um parâmetro ao construtor. OListAdaptervai usar isso para descobrir o que mudou na lista. A assinatura da classeSleepNightAdapterfinalizada vai ficar assim:
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {- Na classe
SleepNightAdapter, exclua o campodata, incluindo o setter. Você não precisa mais dele, porque oListAdapteracompanha a lista para você. - Exclua a substituição de
getItemCount(), porque oListAdapterimplementa esse método para você. - Para se livrar do erro em
onBindViewHolder(), mude a variávelitem. Em vez de usardatapara receber umitem, chame o métodogetItem(position)fornecido peloListAdapter.
val item = getItem(position)Etapa 2: use submitList() para manter a lista atualizada
Seu código precisa informar ao ListAdapter quando uma lista alterada estiver disponível. O ListAdapter fornece um método chamado submitList() para informar ao ListAdapter que uma nova versão da lista está disponível. Quando esse método é chamado, o ListAdapter compara a nova lista com a antiga e detecta itens que foram adicionados, removidos, movidos ou alterados. Em seguida, o ListAdapter atualiza os itens mostrados por RecyclerView.
- Abra
SleepTrackerFragment.kt. - Em
onCreateView(), no observador emsleepTrackerViewModel, encontre o erro em que a variáveldataexcluída é referenciada. - Substitua
adapter.data = itpor uma chamada paraadapter.submitList(it). Confira o código atualizado abaixo.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})- Execute o app. Ele vai rodar mais rápido, talvez não de forma perceptível se a lista for pequena.
Nesta tarefa, você vai usar a mesma técnica dos codelabs anteriores para configurar a vinculação de dados e eliminar as chamadas para findViewById().
Etapa 1: adicionar vinculação de dados ao arquivo de layout
- Abra o arquivo de layout
list_item_sleep_night.xmlna guia Texto. - Coloque o cursor na tag
ConstraintLayoute pressioneAlt+Enter(Option+Enterem um Mac). O menu de intenções (menu de "correção rápida") é aberto. - Selecione Converter em layout de vinculação de dados. Isso envolve o layout em
<layout>e adiciona uma tag<data>dentro. - Role de volta para o topo, se necessário, e dentro da tag
<data>, declare uma variável chamadasleep. - Faça com que o
typeseja o nome totalmente qualificado deSleepNight,com.example.android.trackmysleepquality.database.SleepNight. A tag<data>finalizada vai ficar assim:
<data>
<variable
name="sleep"
type="com.example.android.trackmysleepquality.database.SleepNight"/>
</data>- Para forçar a criação do objeto
Binding, selecione Build > Clean Project e depois Build > Rebuild Project. Se você ainda tiver problemas, selecione File > Invalidate Caches / Restart. O objeto de vinculaçãoListItemSleepNightBinding, junto com o código relacionado, é adicionado aos arquivos gerados do projeto.
Etapa 2: inflar o layout do item usando a vinculação de dados
- Abra
SleepNightAdapter.kt. - Na classe
ViewHolder, encontre o métodofrom(). - Exclua a declaração da variável
view.
Código a ser excluído:
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)- Onde estava a variável
view, defina uma nova variável chamadabindingque infla o objeto de vinculaçãoListItemSleepNightBinding, conforme mostrado abaixo. Faça a importação necessária do objeto de vinculação.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)- No final da função, em vez de retornar o
view, retornebinding.
return ViewHolder(binding)- Para se livrar do erro, coloque o cursor na palavra
binding. PressioneAlt+Enter(Option+Enterem um Mac) para abrir o menu de intenção.
- Selecione Mudar o tipo de parâmetro "itemView" do construtor principal da classe "ViewHolder" para "ListItemSleepNightBinding". Isso atualiza o tipo de parâmetro da classe
ViewHolder.

- Role para cima até a definição da classe
ViewHolderpara ver a mudança na assinatura. Você vai encontrar um erro emitemViewporque mudouitemViewparabindingno métodofrom().
Na definição da classeViewHolder, clique com o botão direito do mouse em uma das ocorrências deitemViewe selecione Refactor > Rename. Mude o nome parabinding. - Adicione o prefixo
valao parâmetro do construtorbindingpara transformá-lo em uma propriedade. - Na chamada para a classe mãe,
RecyclerView.ViewHolder, mude o parâmetro debindingparabinding.root. É necessário transmitir umView, ebinding.rooté oConstraintLayoutraiz no layout do item. - A declaração de classe finalizada vai ficar parecida com o código abaixo.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){Você também vai encontrar um erro nas chamadas para findViewById(), que será corrigido em seguida.
Etapa 3: substituir findViewById()
Agora é possível atualizar as propriedades sleepLength, quality e qualityImage para usar o objeto binding em vez de findViewById().
- Mude as inicializações de
sleepLength,qualityStringequalityImagepara usar as visualizações do objetobinding, conforme mostrado abaixo. Depois disso, o código não vai mais mostrar erros.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImageCom o objeto de vinculação no lugar, não é mais necessário definir as propriedades sleepLength, quality e qualityImage. O DataBinding armazena em cache as pesquisas, então não é necessário declarar essas propriedades.
- Clique com o botão direito do mouse nos nomes das propriedades
sleepLength,qualityequalityImage. Selecione Refatorar > Inline ou pressioneControl+Command+N(Option+Command+Nem um Mac).
- Execute o app. Talvez seja necessário limpar e recompilar o projeto se ele tiver erros.
Nesta tarefa, você vai atualizar o app para usar a vinculação de dados com adaptadores de vinculação e definir os dados nas visualizações.
Em um codelab anterior, você usou a classe Transformations para usar LiveData e gerar strings formatadas para mostrar em visualizações de texto. No entanto, se você precisar vincular tipos diferentes ou complexos, forneça adaptadores de vinculação para ajudar a vinculação de dados a usar esses tipos. Os adaptadores de vinculação são adaptadores que pegam seus dados e os adaptam para algo que a vinculação de dados possa usar para vincular uma visualização, como texto ou uma imagem.
Você vai implementar três adaptadores de vinculação, um para a imagem de qualidade e um para cada campo de texto. Em resumo, para declarar um adaptador de vinculação, defina um método que receba um item e uma visualização e adicione a anotação @BindingAdapter. No corpo do método, implemente a transformação. Em Kotlin, você pode escrever um adaptador de vinculação como uma função de extensão na classe de visualização que recebe os dados.
Etapa 1: criar adaptadores de vinculação
Observe que você terá que importar várias classes na etapa, e elas não serão chamadas individualmente.
- Abra
SleepNightAdapater.kt. - Na classe
ViewHolder, encontre o métodobind()e lembre-se da função dele. Você vai usar o código que calcula os valores debinding.sleepLength,binding.qualityebinding.qualityImagedentro do adaptador. Por enquanto, deixe o código como está. Você vai movê-lo em uma etapa posterior. - No pacote
sleeptracker, crie e abra um arquivo chamadoBindingUtils.kt. - Declare uma função de extensão em
TextView, chamadasetSleepDurationFormatted, e transmita umaSleepNight. Essa função será seu adaptador para calcular e formatar a duração do sono.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}- No corpo de
setSleepDurationFormatted, vincule os dados à visualização como fez emViewHolder.bind(). ChameconvertDurationToFormatted()e defina otextdoTextViewcomo o texto formatado. Como essa é uma função de extensão emTextView, você pode acessar diretamente a propriedadetext.
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)- Para informar a vinculação de dados sobre esse adaptador de vinculação, faça a anotação
@BindingAdapterna função. - Essa função é o adaptador do atributo
sleepDurationFormatted. Portanto, transmitasleepDurationFormattedcomo um argumento para@BindingAdapter.
@BindingAdapter("sleepDurationFormatted")- O segundo adaptador define a qualidade do sono com base no valor de um objeto
SleepNight. Crie uma função de extensão chamadasetSleepQualityString()emTextViewe transmita umaSleepNight. - No corpo, vincule os dados à visualização como fez em
ViewHolder.bind(). ChameconvertNumericQualityToStringe defina otext. - Anote a função com
@BindingAdapter("sleepQualityString").
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}- O terceiro adaptador de vinculação define a imagem em uma visualização de imagem. Crie a função de extensão em
ImageView, chamesetSleepImagee use o código deViewHolder.bind(), conforme mostrado abaixo.
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
}Etapa 2: atualizar o SleepNightAdapter
- Abra
SleepNightAdapter.kt. - Exclua tudo no método
bind(), porque agora você pode usar a vinculação de dados e seus novos adaptadores para fazer esse trabalho.
fun bind(item: SleepNight) {
}- Em
bind(), atribua sleep aitem, porque é necessário informar ao objeto de vinculação sobre seu novoSleepNight.
binding.sleep = item- Abaixo dessa linha, adicione
binding.executePendingBindings(). Essa chamada é uma otimização que pede à vinculação de dados para executar imediatamente todas as vinculações pendentes. É sempre recomendável chamarexecutePendingBindings()ao usar adaptadores de vinculação em umRecyclerView, porque isso pode acelerar um pouco o dimensionamento das visualizações.
binding.executePendingBindings()Etapa 3: adicionar vinculações ao layout XML
- Abra
list_item_sleep_night.xml. - No
ImageView, adicione uma propriedadeappcom o mesmo nome do adaptador de vinculação que define a imagem. Transmita a variávelsleep, conforme mostrado abaixo.
Essa propriedade cria a conexão entre a visualização e o objeto de vinculação pelo adaptador. Sempre quesleepImagefor referenciado, o adaptador vai adaptar os dados doSleepNight.
app:sleepImage="@{sleep}"- Faça o mesmo para as visualizações de texto
sleep_lengthequality_string. Sempre quesleepDurationFormattedousleepQualityStringforem referenciados, os adaptadores vão adaptar os dados doSleepNight.
app:sleepDurationFormatted="@{sleep}"app:sleepQualityString="@{sleep}"- Execute o app. Ele vai funcionar exatamente como antes. Os adaptadores de vinculação cuidam de todo o trabalho de formatação e atualização das visualizações à medida que os dados mudam, simplificando o
ViewHoldere ao código uma estrutura muito melhor do que antes.
Você mostrou a mesma lista nos últimos exercícios. Isso é proposital para mostrar que a interface Adapter permite arquitetar seu código de várias maneiras diferentes. Quanto mais complexo for o código, mais importante será arquitetá-lo bem. Em apps de produção, esses e outros padrões são usados com RecyclerView. Todos os padrões funcionam, e cada um tem seus benefícios. A escolha depende do que você está criando.
Parabéns! Agora você está no caminho certo para dominar o RecyclerView no Android.
Projeto do Android Studio: RecyclerViewDiffUtilDataBinding.
DiffUtil:
- O
RecyclerViewtem uma classe chamadaDiffUtil, que calcula as diferenças entre duas listas. - O
DiffUtiltem uma classe chamadaItemCallBackque você estende para descobrir a diferença entre duas listas. - Na classe
ItemCallback, substitua os métodosareItemsTheSame()eareContentsTheSame().
ListAdapter:
- Para ter um gerenciamento de listas sem custo financeiro, use a classe
ListAdapterem vez deRecyclerView.Adapter. No entanto, se você usarListAdapter, vai precisar escrever seu próprio adaptador para outros layouts. Por isso, este codelab mostra como fazer isso. - Para abrir o menu de intenção no Android Studio, coloque o cursor em qualquer item de código e pressione
Alt+Enter(Option+Enterem um Mac). Esse menu é especialmente útil para refatorar código e criar stubs para implementar métodos. O menu é sensível ao contexto. Por isso, posicione o cursor exatamente para abrir o menu correto.
Vinculação de dados:
- Use a vinculação de dados no layout do item para vincular dados às visualizações.
Adaptadores de vinculação:
- Antes, você usou
Transformationspara criar strings com base em dados. Se você precisar vincular dados de tipos diferentes ou complexos, forneça adaptadores de vinculação para ajudar a vinculação de dados a usá-los. - Para declarar um adaptador de vinculação, defina um método que receba um item e uma visualização e anote o método com
@BindingAdapter. No Kotlin, é possível escrever o adaptador de vinculação como uma função de extensão noView. Transmita o nome da propriedade que o adaptador adapta. Exemplo:
@BindingAdapter("sleepDurationFormatted")- No layout XML, defina uma propriedade
appcom o mesmo nome do adaptador de vinculação. Transmita uma variável com os dados. Exemplo:
.app:sleepDurationFormatted="@{sleep}"Cursos da Udacity:
Documentação do desenvolvedor Android:
- Como criar uma lista com o RecyclerView
RecyclerViewDiffUtil- Biblioteca Data Binding
- Como vincular adaptadores
notifyDataSetChanged()Transformations
Outros recursos:
Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:
- Atribuir o dever de casa, se necessário.
- Informar aos alunos como enviar deveres de casa.
- Atribuir nota aos deveres de casa.
Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.
Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.
Responda estas perguntas
Pergunta 1
Quais das seguintes opções são necessárias para usar DiffUtil? Selecione todas as opções aplicáveis.
▢ Estenda a classe ItemCallBack.
▢ Substituir areItemsTheSame().
▢ Substituir areContentsTheSame().
▢ Use a vinculação de dados para rastrear as diferenças entre os itens.
Pergunta 2
Quais das alternativas a seguir são verdadeiras sobre os adaptadores de vinculação?
▢ Um adaptador de vinculação é uma função anotada com @BindingAdapter.
▢ O uso de um adaptador de vinculação permite separar a formatação de dados do armazenador de visualização.
▢ Você precisa usar um RecyclerViewAdapter se quiser usar adaptadores de vinculação.
▢ Os adaptadores de vinculação são uma boa solução quando você precisa transformar dados complexos.
Pergunta 3
Quando você deve usar Transformations em vez de um adaptador de vinculação? Selecione todas as opções aplicáveis.
▢ Seus dados são simples.
▢ Você está formatando uma string.
▢ Sua lista é muito longa.
▢ Seu ViewHolder contém apenas uma visualização.
Comece a próxima lição:

