Conceitos básicos do Kotlin para Android 07.4: como interagir com itens do RecyclerView

Este codelab faz parte do curso Conceitos básicos do Kotlin para Android. Você aproveitará mais o curso se fizer os codelabs em sequência. Todos os codelabs do curso estão listados na página de destino dos codelabs do curso Conceitos básicos do Kotlin para Android.

Introdução

A maioria dos apps que usam listas e grades que exibem itens permitem que os usuários interajam com eles. Tocar em um item da lista e ver os detalhes dele é um caso de uso muito comum para esse tipo de interação. Para isso, você pode adicionar listeners de cliques que respondem aos toques do usuário em itens mostrando uma visualização detalhada.

Neste codelab, você adicionará interação ao RecyclerView, com base em uma versão estendida do app monitor de sono da série anterior de codelabs.

O que você já precisa saber

  • Criação de uma interface do usuário básica usando uma atividade, fragmentos e visualizações.
  • Navegar entre fragmentos usando safeArgs para transmitir dados entre eles.
  • Veja modelos, visualizações de fábricas, transformações e LiveData e os respectivos observadores.
  • Como criar um banco de dados Room, um objeto de acesso a dados (DAO, na sigla em inglês) e definir entidades.
  • Como usar corrotinas para banco de dados e outras tarefas de longa duração.
  • Como implementar um RecyclerView básico com um Adapter, ViewHolder e o layout do item.
  • Como implementar a vinculação de dados para a RecyclerView.
  • Como criar e usar adaptadores de vinculação para transformar dados.
  • Como usar GridLayoutManager.

O que você vai aprender

  • Como tornar os itens no RecyclerView clicáveis. Implemente um listener de clique para navegar até uma visualização detalhada quando um item for clicado.

Atividades do laboratório

  • Crie em uma versão estendida do app TrackMySleepQuality do codelab anterior desta série.
  • Adicione um listener de cliques à lista e comece a ouvir a interação do usuário. Quando um item da lista é tocado, ele aciona a navegação para um fragmento com detalhes sobre o item clicado. O código inicial fornece o código para o fragmento de detalhes, bem como o código de navegação.

O app inicial do rastreador de sono tem duas telas, representadas por fragmentos, como mostrado na figura abaixo.

A primeira tela, mostrada à esquerda, tem botões para iniciar e interromper o rastreamento. A tela mostra alguns dados de sono do usuário. O botão Limpar exclui permanentemente todos os dados que o app coletou para o usuário. A segunda tela, mostrada à direita, serve para selecionar uma classificação de qualidade do sono.

Esse app usa uma arquitetura simplificada com um controlador de IU, modelo de visualização e LiveData, além de um banco de dados Room para manter os dados de sono.

Neste codelab, você adicionará a capacidade de responder quando um usuário tocar em um item na grade, o que exibirá uma tela de detalhes como a mostrada abaixo. O código dessa tela (fragmento, modelo de visualização e navegação) é fornecido com o app inicial, e você implementará o mecanismo de manipulação de cliques.

Etapa 1: fazer o download do app inicial

  1. Faça o download do código RecyclerViewClickHandler-Starter no GitHub e abra o projeto no Android Studio.
  2. Crie e execute o app do rastreador de sono inicial.

[Opcional] Atualize seu app se quiser usá-lo do codelab anterior.

Se você for trabalhar no app inicial fornecido no GitHub para este codelab, pule para a próxima etapa.

Se você quiser continuar usando seu próprio app de monitoramento de sono criado no codelab anterior, siga as instruções abaixo para atualizar o app existente para que ele tenha o código do fragmento da tela de detalhes.

  1. Mesmo que você continue trabalhando no app, faça o download do código RecyclerViewClickHandler-Starter no GitHub para copiar os arquivos.
  2. Copie todos os arquivos no pacote sleepdetail.
  3. Na pasta layout, copie o arquivo fragment_sleep_detail.xml.
  4. Copie o conteúdo atualizado de navigation.xml, que adiciona a navegação para o sleep_detail_fragment.
  5. No pacote database, no SleepDatabaseDao, adicione o novo método getNightWithId():
/**
 * Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
  1. No res/values/strings, adicione o seguinte recurso de string:
<string name="close">Close</string>
  1. Limpe e recrie seu app para atualizar a vinculação de dados.

Etapa 2: inspecionar o código da tela de detalhes do sono

Neste codelab, você vai implementar um gerenciador de cliques que navega para um fragmento que mostra detalhes sobre a noite de sono clicada. O código inicial já contém o fragmento e o gráfico de navegação deste SleepDetailFragment, porque são bastante códigos, e os fragmentos e a navegação não fazem parte deste codelab. Familiarize-se com o seguinte código:

  1. No app, localize o pacote sleepdetail. Este pacote contém o fragmento, o modelo de visualização e a fábrica de modelo de visualização de um fragmento que exibe detalhes de uma noite de sono.

  2. No pacote sleepdetail, abra e inspecione o código do SleepDetailViewModel. Esse modelo de visualização usa a chave de um SleepNight e um DAO no construtor.

    O corpo da classe tem código para receber o SleepNight da chave especificada e a variável navigateToSleepTracker para controlar a navegação de volta para o SleepTrackerFragment quando o botão Fechar é pressionado.

    A função getNightWithId() retorna um LiveData<SleepNight> e é definida em SleepDatabaseDao (no pacote database).

  3. No pacote sleepdetail, abra e inspecione o código do SleepDetailFragment. Observe a configuração da vinculação de dados, o modelo de visualização e o observador para navegação.

  4. No pacote sleepdetail, abra e inspecione o código do SleepDetailViewModelFactory.

  5. Na pasta do layout, inspecione fragment_sleep_detail.xml. Observe a variável sleepDetailViewModel definida na tag <data> para que os dados sejam exibidos em cada visualização do modelo de visualização.

    O layout contém um ConstraintLayout com uma ImageView para a qualidade do sono, um TextView para uma classificação de qualidade, um TextView para a duração do sono e um Button para fechar o fragmento de detalhes.

  6. Abra o arquivo navigation.xml. Para o sleep_tracker_fragment, observe a nova ação para sleep_detail_fragment.

    A nova ação, action_sleep_tracker_fragment_to_sleepDetailFragment, é a navegação do fragmento do rastreador de sono para a tela de detalhes.

Nesta tarefa, você atualizará o RecyclerView para responder aos toques do usuário mostrando uma tela de detalhes para o item tocado.

O recebimento e o processamento de cliques são tarefas de duas partes: primeiro, você precisa detectar e receber o clique e determinar qual item foi clicado. Em seguida, responda ao clique com uma ação.

Então, qual é o melhor lugar para adicionar um listener de clique para esse aplicativo?

  • O SleepTrackerFragment hospeda muitas visualizações. Portanto, a detecção de eventos de clique no nível do fragmento não informará qual item foi clicado. Ele nem saberá se foi um item clicado ou um dos outros elementos da IU.
  • Ouvir o nível RecyclerView, é difícil descobrir exatamente em qual item da lista o usuário clicou.
  • O melhor ritmo para ver informações sobre um item clicado é no objeto ViewHolder, já que ele representa um item da lista.

Embora o ViewHolder seja um ótimo local para detectar cliques, ele geralmente não é o lugar certo para processá-los. Então, qual é o melhor lugar para gerenciar os cliques?

  • O Adapter exibe itens de dados em visualizações para que você possa processar os cliques no adaptador. No entanto, a tarefa do adaptador é adaptar os dados para exibição, não lidar com a lógica do app.
  • Geralmente, você precisa gerenciar cliques no ViewModel, porque o ViewModel tem acesso aos dados e à lógica para determinar o que precisa acontecer em resposta ao clique.

Etapa 1: criar um listener de clique e acioná-lo no layout do item

  1. Na pasta sleeptracker, abra SleepNightAdapter.kt.
  2. No final do arquivo, no nível superior, crie uma nova classe de listener, SleepNightListener.
class SleepNightListener() {
    
}
  1. Na classe SleepNightListener, adicione uma função onClick(). Quando a visualização que exibe um item da lista é clicada, ela chama essa função onClick(). Você definirá a propriedade android:onClick da visualização posteriormente para essa função.
class SleepNightListener() {
    fun onClick() = 
}
  1. Adicione um argumento de função night do tipo SleepNight a onClick(). A visualização sabe qual item está sendo exibido e as informações precisam ser transmitidas para processar o clique.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Para definir o que o onClick() faz, forneça um callback clickListener no construtor da SleepNightListener e atribua-o à onClick().

    Atribuir o lambda que gerencia o nome do clique, clickListener , ajuda a monitorá-lo conforme ele é transmitido entre as classes. O callback clickListener só precisa que o night.nightId acesse dados do banco de dados. A classe SleepNightListener concluída ficará parecida com o código abaixo.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Abra list_item_sleep_night.xml.
  2. No bloco data, adicione uma nova variável para disponibilizar a classe SleepNightListener usando a vinculação de dados. Defina o novo <variable> como um name de clickListener. e defina o type como o nome totalmente qualificado da classe com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, conforme mostrado abaixo. Agora você pode acessar a função onClick() no SleepNightListener usando esse layout.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Para ouvir cliques em qualquer parte desse item da lista, adicione o atributo android:onClick ao ConstraintLayout.

    Defina o atributo como clickListener:onClick(sleep) usando um lambda de vinculação de dados, conforme mostrado abaixo:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Etapa 2: transmita o listener de clique para o fixador de visualização e o objeto de vinculação

  1. Abra SleepNightAdapter.kt.
  2. Modifique o construtor da classe SleepNightAdapter para receber um val clickListener: SleepNightListener. Quando o adaptador vincular a ViewHolder, ele precisará fornecê-lo com esse listener de clique.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. No onBindViewHolder(), atualize a chamada para holder.bind() e transmita o listener de clique para o ViewHolder. Você verá um erro do compilador porque adicionou um parâmetro à chamada de função.
holder.bind(getItem(position)!!, clickListener)
  1. Adicione o parâmetro clickListener à bind(). Para fazer isso, posicione o cursor sobre o erro e pressione Alt+Enter (Windows) ou Option+Enter (Mac) no erro como , conforme a captura de tela abaixo.

  1. Na classe ViewHolder, na função bind(), atribua o listener de clique ao objeto binding. Você vê um erro porque precisa atualizar o objeto de vinculação.
binding.clickListener = clickListener
  1. Para atualizar a vinculação de dados, limpe e recrie seu projeto. Talvez você também precise invalidar os caches. Então, você pegou um listener de clique do construtor do adaptador e o passou para o armazenador de visualização e o objeto de vinculação.

Etapa 3: mostrar um aviso quando um item for tocado

Agora você tem o código configurado para capturar um clique, mas não implementou o que acontece quando um item da lista é tocado. A resposta mais simples é exibir um aviso mostrando o nightId quando um item for clicado. Isso verifica se, quando um item da lista é clicado, o nightId correto é capturado e transmitido.

  1. Abra SleepTrackerFragment.kt.
  2. Em onCreateView(), encontre a variável adapter. Observe que ele mostra um erro, já que espera um parâmetro de listener de cliques.
  3. Defina um listener de clique transmitindo uma lambda para o SleepNightAdapter. Esse lambda simples exibe apenas um aviso mostrando o nightId, conforme mostrado abaixo. Você precisará importar Toast. Veja abaixo a definição atualizada completa.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Execute o app, toque em itens e verifique se eles exibem um aviso com o nightId correto. Como os itens têm valores nightId crescentes e o app exibe a noite mais recente primeiro, o item com o menor valor nightId está na parte inferior da lista.

Nesta tarefa, você mudará o comportamento quando um item na RecyclerView for clicado, de modo que, em vez de mostrar um aviso, o app navegue até um fragmento de detalhes que mostra mais informações sobre a noite clicada.

Etapa 1: navegar com um clique

Nesta etapa, em vez de apenas exibir um aviso, você mudará o lambda do listener de clique no onCreateView() do SleepTrackerFragment para transmitir o nightId para o SleepTrackerViewModel e acionar a navegação para o SleepDetailFragment.

Defina a função do gerenciador de cliques:

  1. Abra o SleepTrackerViewModel.kt.
  2. Dentro de SleepTrackerViewModel, defina a função de gerenciador de cliques onSleepNightClicked() no final.
fun onSleepNightClicked(id: Long) {

}
  1. Dentro do onSleepNightClicked(), acione a navegação definindo _navigateToSleepDetail como o id transmitido da noite de sono clicada.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. implementar _navigateToSleepDetail; Como você fez anteriormente, defina um private MutableLiveData para o estado de navegação. E um gettable público do val para acompanhar.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Defina o método a ser chamado depois que o app termina de navegar. Dê o nome onSleepDetailNavigated() a ela e defina o valor como null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Adicione o código para chamar o gerenciador de cliques:

  1. Abra SleepTrackerFragment.kt, role para baixo até o código que cria o adaptador e define SleepNightListener para mostrar um aviso.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Adicione o seguinte código abaixo da notificação toast para chamar um gerenciador de cliques, onSleepNighClicked(), no sleepTrackerViewModel quando um item for tocado. Passe o nightId para que o modelo de visualização saiba qual noite de sono dormir. Isso gera um erro, porque você ainda não definiu onSleepNightClicked(). Você pode manter, comentar ou excluir o aviso.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Adicione o código para observar os cliques:

  1. Abra SleepTrackerFragment.kt.
  2. No onCreateView(), logo acima da declaração do manager, adicione o código para observar o novo LiveData do navigateToSleepDetail. Quando o navigateToSleepDetail mudar, acesse o SleepDetailFragment, transmitindo a night e, em seguida, chame o onSleepDetailNavigated(). Como você já fez isso em um codelab anterior, este é o código:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Execute o código, clique em um item e ... o app falha.

Processar valores nulos nos adaptadores de vinculação:

  1. Execute o app novamente no modo de depuração. Toque em um item e filtre os registros para mostrar erros. Ele mostrará um stack trace incluindo o que está abaixo.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

Infelizmente, o stack trace não deixa claro onde esse erro é acionado. Uma desvantagem da vinculação de dados é que ela pode dificultar a depuração do seu código. O app falha quando você clica em um item e o único código novo é para processar o clique.

No entanto, esse novo mecanismo de gerenciamento de cliques permite que os adaptadores de vinculação sejam chamados com um valor null para item. Em particular, quando o app é iniciado, a LiveData é iniciada como null. Portanto, é necessário adicionar verificações de nulidade a cada um dos adaptadores.

  1. Em BindingUtils.kt, para cada adaptador de vinculação, mude o tipo de argumento item para anulável e envolva o corpo com item?.let{...}. Por exemplo, o adaptador para a sleepQualityString ficará assim. Mude os outros adaptadores da mesma maneira.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Execute o app. Toque em um item para abrir a visualização de detalhes.

Projeto do Android Studio: RecyclerViewClickHandler.

Para fazer com que os itens em uma RecyclerView respondam a cliques, anexe listeners de clique para listar itens no ViewHolder e processar cliques no ViewModel.

Para fazer com que os itens em uma RecyclerView respondam a cliques, é necessário fazer o seguinte:

  • Crie uma classe de listener que use um lambda e o atribua a uma função onClick().
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Defina o listener de clique na visualização.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Transmita o listener de clique para o construtor do adaptador no armazenador de visualização e adicione-o ao objeto de vinculação.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • No fragmento que mostra a visualização de reciclagem, em que você cria o adaptador, defina um listener de clique transmitindo uma lambda para o adaptador.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Implemente o gerenciador de cliques no modelo de visualização. Para cliques em itens da lista, isso geralmente aciona a navegação para um fragmento de detalhes.

Curso da Udacity:

Documentação do desenvolvedor Android:

Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:

  • Se necessário, atribua o dever de casa.
  • Informe aos alunos como enviar o dever de casa.
  • Atribua nota aos trabalhos de casa.

Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.

Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.

Responda a estas perguntas

Pergunta 1

Suponha que seu app tenha um RecyclerView que exibe itens em uma lista de compras. O app também define uma classe de listener de cliques:

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
    fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

Como você disponibiliza o ShoppingListItemListener para a vinculação de dados? Escolha uma opção.

▢ No arquivo de layout que contém a RecyclerView que exibe a lista de compras, adicione uma variável <data> para ShoppingListItemListener.

▢ No arquivo de layout que define o layout de uma única linha na lista de compras, adicione uma variável <data> para ShoppingListItemListener.

▢ Na classe ShoppingListItemListener, adicione uma função para ativar a vinculação de dados:

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}

▢ Na classe ShoppingListItemListener, na função onClick(), adicione uma chamada para ativar a vinculação de dados:

fun onClick(cartItem: CartItem) = { 
    clickListener(cartItem.itemId)
    dataBindingEnable(true)
}

Pergunta 2

Onde você adiciona o atributo android:onClick para que os itens em uma RecyclerView respondam aos cliques? Selecione todas as opções aplicáveis.

▢ No arquivo de layout que exibe RecyclerView, adicione-o a <androidx.recyclerview.widget.RecyclerView>.

▢ Adicione-o ao arquivo de layout de um item da linha. Se você quiser que o item inteiro seja clicável, adicione-a à visualização mãe que contém os itens na linha.

▢ Adicione-o ao arquivo de layout de um item da linha. Se você quiser que um único TextView no item seja clicável, adicione-o à <TextView>.

▢ Sempre adicione-o ao arquivo de layout da MainActivity.

Inicie a próxima lição: 7.5: Cabeçalhos no RecyclerView