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

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

A maioria dos apps que usam listas e grades para mostrar itens permite que os usuários interajam com eles. Tocar em um item de uma lista e ver os detalhes dele é um caso de uso muito comum para esse tipo de interação. Para isso, adicione listeners de clique que respondam aos toques do usuário em itens mostrando uma visualização de detalhes.

Neste codelab, você vai adicionar interação ao seu RecyclerView, criando uma versão estendida do app de rastreamento do sono da série anterior de codelabs.

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 safeArgs para transmitir dados entre eles.
  • Ver modelos, fábricas de modelos, transformações, LiveData e os observadores deles.
  • Como criar um banco de dados Room, um objeto de acesso a dados (DAO) 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 um layout de item.
  • Como implementar a vinculação de dados para RecyclerView.
  • Como criar e usar adaptadores de vinculação para transformar dados.
  • Como usar o GridLayoutManager.

O que você vai aprender

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

Atividades deste laboratório

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

O app rastreador de sono inicial 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 usa uma arquitetura simplificada com um controlador de interface, um modelo de visualização e LiveData, além de um banco de dados Room para manter os dados de sono.

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

Etapa 1: instalar o app inicial

  1. Faça o download do código inicial do RecyclerViewClickHandler (link em inglês) no GitHub e abra o projeto no Android Studio.
  2. Crie e execute o app inicial de rastreamento do sono.

[Opcional] Atualize o app se quiser usar o app do codelab anterior

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

Se você quiser continuar usando o app de monitoramento do sono criado no codelab anterior, siga as instruções abaixo para atualizar o app e incluir o código do fragmento da tela de detalhes.

  1. Mesmo que você esteja continuando com seu app atual, baixe o código RecyclerViewClickHandler-Starter do GitHub para copiar os arquivos.
  2. Copie todos os arquivos do 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, em 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. Em res/values/strings, adicione o seguinte recurso de string:
<string name="close">Close</string>
  1. Limpe e recrie o 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 manipulador de cliques que navega até um fragmento com detalhes sobre a noite de sono clicada. O código inicial já contém o fragmento e o gráfico de navegação para essa SleepDetailFragment, porque é muito código, e fragmentos e navegação não fazem parte deste codelab. Conheça o seguinte código:

  1. No app, encontre o pacote sleepdetail. Esse pacote contém o fragmento, o ViewModel e a fábrica de ViewModel para um fragmento que mostra 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 ao 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, do modelo de visualização e do observador para navegação.

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

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

    O layout contém um ConstraintLayout com um 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 do 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ê vai atualizar o RecyclerView para responder aos toques do usuário mostrando uma tela de detalhes do item tocado.

Receber e processar cliques é uma tarefa dividida em duas partes: primeiro, você precisa ouvir 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 app?

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

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

  • O Adapter mostra 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 processar cliques no ViewModel porque ele tem acesso aos dados e à lógica para determinar o que precisa acontecer em resposta ao clique.ViewModel

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

  1. Na pasta sleeptracker, abra SleepNightAdapter.kt.
  2. No fim 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 mostra um item da lista é clicada, ela chama essa função onClick(). Você vai definir a propriedade android:onClick da visualização mais tarde 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á mostrando, e essas informações precisam ser transmitidas para processar o clique.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Para definir o que onClick() faz, forneça um callback clickListener no construtor de SleepNightListener e atribua-o a onClick().

    Dar um nome à lambda que processa o clique, clickListener , ajuda a acompanhar a função à medida que ela é transmitida entre as classes. O callback clickListener só precisa do night.nightId para acessar dados do banco de dados. A classe SleepNightListener finalizada vai ficar assim:
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. Dê ao novo <variable> um name de clickListener.. 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() em SleepNightListener usando esse layout.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Para detectar cliques em qualquer parte desse item da lista, adicione o atributo android:onClick ao ConstraintLayout.

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

Etapa 2: transmitir o listener de clique ao holder de visualização e ao objeto de vinculação

  1. Abra SleepNightAdapter.kt.
  2. Modifique o construtor da classe SleepNightAdapter para receber um val clickListener: SleepNightListener. Quando o adaptador vincula o ViewHolder, ele precisa fornecer a ele esse listener de clique.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. Em onBindViewHolder(), atualize a chamada para holder.bind() para também transmitir o listener de clique para o ViewHolder. Você vai receber um erro do compilador porque adicionou um parâmetro à chamada de função.
holder.bind(getItem(position)!!, clickListener)
  1. Adicione o parâmetro clickListener a bind(). Para fazer isso, coloque o cursor sobre o erro e pressione Alt+Enter (Windows) ou Option+Enter (Mac), conforme mostrado na captura de tela abaixo.

  1. Na classe ViewHolder, dentro da função bind(), atribua o listener de clique ao objeto binding. Você vai receber um erro porque precisa atualizar o objeto de vinculação.
binding.clickListener = clickListener
  1. Para atualizar a vinculação de dados, limpe e recompile seu projeto. Talvez seja necessário invalidar os caches também. Você pegou um listener de clique do construtor do adaptador e o transmitiu até o fixador de visualização e o objeto de vinculação.

Etapa 3: mostrar um toast quando um item for tocado

Agora você tem o código para capturar um clique, mas ainda não implementou o que acontece quando um item da lista é tocado. A resposta mais simples é mostrar um toast com o nightId quando um item é 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. Ele mostra um erro porque agora espera um parâmetro de listener de clique.
  3. Defina um listener de clique transmitindo uma lambda para o SleepNightAdapter. Essa lambda simples apenas mostra um toast com o nightId, conforme mostrado abaixo. Você precisará importar Toast. Confira abaixo a definição completa e atualizada.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Execute o app, toque nos itens e verifique se eles mostram um toast com o nightId correto. Como os itens têm valores de nightId cada vez maiores, e o app mostra a noite mais recente primeiro, o item com o menor nightId fica na parte de baixo da lista.

Nesta tarefa, você vai mudar o comportamento quando um item no RecyclerView é clicado. Em vez de mostrar um toast, o app vai navegar até um fragmento de detalhes que mostra mais informações sobre a noite clicada.

Etapa 1: navegar ao clicar

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

Defina a função de manipulador de cliques:

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

}
  1. Dentro da onSleepNightClicked(), acione a navegação definindo _navigateToSleepDetail como a id transmitida da noite de sono clicada.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. implementar _navigateToSleepDetail; Como você já fez antes, defina um private MutableLiveData para o estado de navegação. e um val público para acompanhar.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Defina o método a ser chamado depois que o app terminar de navegar. Chame-o de onSleepDetailNavigated() 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 e role a tela para baixo até o código que cria o adaptador e define SleepNightListener para mostrar um toast.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Adicione o seguinte código abaixo do toast para chamar um gerenciador de cliques, onSleepNighClicked(), no sleepTrackerViewModel quando um item for tocado. Transmita o nightId para que o modelo de visualização saiba qual noite de sono precisa ser buscada. Isso gera um erro, já que você ainda não definiu onSleepNightClicked(). Você pode manter, comentar ou excluir o toast, como quiser.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Adicione o código para observar cliques:

  1. Abra SleepTrackerFragment.kt.
  2. Em onCreateView(), logo acima da declaração de manager, adicione o código para observar o novo navigateToSleepDetail LiveData. Quando navigateToSleepDetail mudar, navegue até o SleepDetailFragment, transmitindo o night e chame onSleepDetailNavigated() depois. Como você já fez isso em um codelab anterior, aqui está 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 os erros. Ele vai mostrar um rastreamento de pilha, incluindo algo como 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 código. O app falha quando você clica em um item, e o único código novo é para processar o clique.

No entanto, com esse novo mecanismo de processamento de cliques, agora é possível que os adaptadores de vinculação sejam chamados com um valor null para item. Em particular, quando o app é iniciado, o LiveData começa como null. Portanto, é necessário adicionar verificações de nulo a cada um dos adaptadores.

  1. Em BindingUtils.kt, para cada um dos adaptadores de vinculação, mude o tipo do argumento item para anulável e encapsule o corpo com item?.let{...}. Por exemplo, seu adaptador para sleepQualityString vai ficar assim: Mude os outros adaptadores da mesma forma.
@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 uma visualização detalhada.

Projeto do Android Studio: RecyclerViewClickHandler (link em inglês).

Para fazer com que os itens em um RecyclerView respondam a cliques, anexe listeners de clique aos itens da lista no ViewHolder e processe os cliques no ViewModel.

Para que os itens em um RecyclerView respondam aos cliques, faça o seguinte:

  • Crie uma classe de listener que receba uma lambda e a 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 ao construtor do adaptador, ao holder 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, onde 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 até um fragmento de detalhes.

Curso da Udacity:

Documentação do desenvolvedor Android:

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

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

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 o RecyclerView que mostra 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, dentro da 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 um RecyclerView respondam aos cliques? Selecione todas as opções aplicáveis.

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

▢ Adicione-a ao arquivo de layout de um item na 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-a ao arquivo de layout de um item na linha. Se você quiser que um único TextView no item seja clicável, adicione-o ao <TextView>.

▢ Sempre adicione-a ao arquivo de layout para a MainActivity.

Comece a próxima lição: 7.5: cabeçalhos no RecyclerView