Android Kotlin Fundamentals 07.4: Interacting with RecyclerView items

Questo codelab fa parte del corso Android Kotlin Fundamentals. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire le codelab in sequenza. Tutti i codelab del corso sono elencati nella pagina di destinazione dei codelab Android Kotlin Fundamentals.

Introduzione

La maggior parte delle app che utilizzano elenchi e griglie che mostrano elementi consente agli utenti di interagire con gli elementi. Toccando un elemento di un elenco e visualizzandone i dettagli, si verifica un caso d'uso molto comune per questo tipo di interazione. Per farlo, puoi aggiungere listener di clic che rispondono ai tocchi degli utenti sugli elementi mostrando una visualizzazione dettagliata.

In questo codelab, aggiungi l'interazione al tuo RecyclerView, basandoti su una versione estesa dell'app per il monitoraggio del sonno della precedente serie di codelab.

Cosa devi già sapere

  • Creazione di un'interfaccia utente di base utilizzando un'attività, frammenti e visualizzazioni.
  • Spostarsi tra i fragment e utilizzare safeArgs per passare i dati tra i fragment.
  • Visualizza modelli, fabbriche di modelli, trasformazioni e LiveData e i relativi osservatori.
  • Come creare un database Room, creare un oggetto di accesso ai dati (DAO) e definire le entità.
  • Come utilizzare le coroutine per il database e altre attività di lunga durata.
  • Come implementare un RecyclerView di base con un layout Adapter, ViewHolder e degli elementi.
  • Come implementare l'associazione di dati per RecyclerView.
  • Come creare e utilizzare gli adattatori di binding per trasformare i dati.
  • Come utilizzare GridLayoutManager.

Obiettivi didattici

  • Come rendere cliccabili gli elementi in RecyclerView. Implementa un listener di clic per passare a una visualizzazione dettagliata quando viene fatto clic su un elemento.

In questo lab proverai a:

  • Sviluppa una versione estesa dell'app TrackMySleepQuality del codelab precedente di questa serie.
  • Aggiungi un listener di clic all'elenco e inizia ad ascoltare l'interazione dell'utente. Quando viene toccato un elemento dell'elenco, viene attivata la navigazione a un frammento con i dettagli dell'elemento su cui è stato fatto clic. Il codice iniziale fornisce il codice per il frammento dei dettagli, nonché il codice di navigazione.

L'app di monitoraggio del sonno iniziale ha due schermate, rappresentate da frammenti, come mostrato nella figura seguente.

La prima schermata, mostrata a sinistra, ha pulsanti per avviare e interrompere il monitoraggio. La schermata mostra alcuni dati sul sonno dell'utente. Il pulsante Cancella elimina definitivamente tutti i dati raccolti dall'app per l'utente. La seconda schermata, mostrata a destra, serve per selezionare una valutazione della qualità del sonno.

Questa app utilizza un'architettura semplificata con un controller UI, un modello di visualizzazione e LiveData e un database Room per archiviare i dati sul sonno.

In questo codelab, aggiungi la possibilità di rispondere quando un utente tocca un elemento nella griglia, visualizzando una schermata dei dettagli come quella riportata di seguito. Il codice per questa schermata (frammento, modello di visualizzazione e navigazione) viene fornito con l'app iniziale e implementerai il meccanismo di gestione dei clic.

Passaggio 1: scarica l'app iniziale

  1. Scarica il codice iniziale RecyclerViewClickHandler da GitHub e apri il progetto in Android Studio.
  2. Crea ed esegui l'app di monitoraggio del sonno iniziale.

[Facoltativo] Aggiorna l'app se vuoi utilizzare quella del codelab precedente

Se utilizzerai l'app iniziale fornita in GitHub per questo codelab, vai al passaggio successivo.

Se vuoi continuare a utilizzare l'app per il monitoraggio del sonno che hai creato nel codelab precedente, segui le istruzioni riportate di seguito per aggiornare l'app esistente in modo che contenga il codice per il fragment della schermata dei dettagli.

  1. Anche se continui a utilizzare la tua app esistente, scarica il codice iniziale RecyclerViewClickHandler da GitHub per poter copiare i file.
  2. Copia tutti i file nel pacchetto sleepdetail.
  3. Nella cartella layout, copia il file fragment_sleep_detail.xml.
  4. Copia i contenuti aggiornati di navigation.xml, che aggiungono la navigazione per sleep_detail_fragment.
  5. Nel pacchetto database, in SleepDatabaseDao, aggiungi il nuovo metodo 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. In res/values/strings aggiungi la seguente risorsa stringa:
<string name="close">Close</string>
  1. Pulisci e ricrea l'app per aggiornare l'associazione di dati.

Passaggio 2: esamina il codice per la schermata dei dettagli del sonno

In questo codelab, implementerai un gestore di clic che passa a un fragment che mostra i dettagli della notte di sonno su cui è stato fatto clic. Il codice iniziale contiene già il fragment e il grafico di navigazione per questo SleepDetailFragment, perché si tratta di una quantità considerevole di codice e i fragment e la navigazione non fanno parte di questo codelab. Familiarizza con il seguente codice:

  1. Nella tua app, trova il pacchetto sleepdetail. Questo pacchetto contiene il fragment, il modello di visualizzazione e la relativa factory per un fragment che mostra i dettagli di una notte di sonno.

  2. Nel pacchetto sleepdetail, apri e controlla il codice per SleepDetailViewModel. Questo modello di visualizzazione accetta la chiave per un SleepNight e un DAO nel costruttore.

    Il corpo della classe contiene il codice per ottenere l'SleepNight per la chiave specificata e la variabile navigateToSleepTracker per controllare la navigazione di ritorno a SleepTrackerFragment quando viene premuto il pulsante Chiudi.

    La funzione getNightWithId() restituisce un valore LiveData<SleepNight> ed è definita in SleepDatabaseDao (nel pacchetto database).

  3. Nel pacchetto sleepdetail, apri e controlla il codice per SleepDetailFragment. Nota la configurazione per il data binding, il modello di visualizzazione e l'observer per la navigazione.

  4. Nel pacchetto sleepdetail, apri e controlla il codice per SleepDetailViewModelFactory.

  5. Nella cartella del layout, esamina fragment_sleep_detail.xml. Nota la variabile sleepDetailViewModel definita nel tag <data> per visualizzare i dati in ogni visualizzazione dal modello di visualizzazione.

    Il layout contiene un ConstraintLayout che contiene un ImageView per la qualità del sonno, un TextView per una valutazione della qualità, un TextView per la durata del sonno e un Button per chiudere il frammento dei dettagli.

  6. Apri il file navigation.xml. Per sleep_tracker_fragment, nota la nuova azione per sleep_detail_fragment.

    La nuova azione, action_sleep_tracker_fragment_to_sleepDetailFragment, è la navigazione dal frammento del monitoraggio del sonno alla schermata dei dettagli.

In questa attività, aggiorna RecyclerView per rispondere ai tocchi dell'utente mostrando una schermata dei dettagli per l'elemento toccato.

La ricezione e la gestione dei clic è un'attività in due parti: innanzitutto, devi ascoltare e ricevere il clic e determinare su quale elemento è stato fatto clic. Dopodiché, devi rispondere al clic con un'azione.

Qual è quindi il posto migliore per aggiungere un listener di clic per questa app?

  • Il SleepTrackerFragment ospita molte visualizzazioni, quindi l'ascolto degli eventi di clic a livello di frammento non ti dirà su quale elemento è stato fatto clic. Non ti dirà nemmeno se si trattava di un elemento su cui è stato fatto clic o di uno degli altri elementi dell'interfaccia utente.
  • Se ascolti a livello di RecyclerView, è difficile capire esattamente su quale elemento dell'elenco ha fatto clic l'utente.
  • Il ritmo migliore per ottenere informazioni su un elemento su cui è stato fatto clic è nell'oggetto ViewHolder, poiché rappresenta una voce di elenco.

Sebbene ViewHolder sia un ottimo posto per ascoltare i clic, di solito non è il posto giusto per gestirli. Qual è quindi il posto migliore per gestire i clic?

  • Adapter mostra gli elementi di dati nelle visualizzazioni, quindi puoi gestire i clic nell'adattatore. Tuttavia, il compito dell'adattatore è adattare i dati per la visualizzazione, non gestire la logica dell'app.
  • In genere, i clic devono essere gestiti in ViewModel, perché ViewModel ha accesso ai dati e alla logica per determinare cosa deve succedere in risposta al clic.

Passaggio 1: crea un listener di clic e attivarlo dal layout dell'elemento

  1. Nella cartella sleeptracker, apri SleepNightAdapter.kt.
  2. Alla fine del file, al primo livello, crea una nuova classe di listener, SleepNightListener.
class SleepNightListener() {
    
}
  1. All'interno della classe SleepNightListener, aggiungi una funzione onClick(). Quando viene fatto clic sulla visualizzazione che mostra un elemento dell'elenco, la visualizzazione chiama questa funzione onClick(). In un secondo momento, imposterai la proprietà android:onClick della vista su questa funzione.
class SleepNightListener() {
    fun onClick() = 
}
  1. Aggiungi un argomento della funzione night di tipo SleepNight a onClick(). La visualizzazione sa quale elemento sta mostrando e queste informazioni devono essere trasmesse per la gestione del clic.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Per definire cosa fa onClick(), fornisci un callback clickListener nel costruttore di SleepNightListener e assegnagli onClick().

    Assegnare un nome alla lambda che gestisce il clic, clickListener , aiuta a tenerne traccia quando viene passata tra le classi. Il callback clickListener richiede solo night.nightId per accedere ai dati del database. La classe SleepNightListener completata dovrebbe avere l'aspetto del codice riportato di seguito.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Apri list_item_sleep_night.xml..
  2. All'interno del blocco data, aggiungi una nuova variabile per rendere disponibile la classe SleepNightListener tramite il data binding. Assegna al nuovo <variable> un name di clickListener. Imposta type sul nome completo della classe com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, come mostrato di seguito. Ora puoi accedere alla funzione onClick() in SleepNightListener da questo layout.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Per rilevare i clic su qualsiasi parte di questo elemento dell'elenco, aggiungi l'attributo android:onClick a ConstraintLayout.

    Imposta l'attributo su clickListener:onClick(sleep) utilizzando una lambda di data binding, come mostrato di seguito:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Passaggio 2: passa il listener di clic al view holder e all'oggetto di binding

  1. Apri SleepNightAdapter.kt.
  2. Modifica il costruttore della classe SleepNightAdapter per ricevere un val clickListener: SleepNightListener. Quando l'adattatore associa ViewHolder, deve fornirgli questo listener dei clic.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. In onBindViewHolder(), aggiorna la chiamata a holder.bind() per passare anche il listener di clic a ViewHolder. Riceverai un errore del compilatore perché hai aggiunto un parametro alla chiamata di funzione.
holder.bind(getItem(position)!!, clickListener)
  1. Aggiungi il parametro clickListener a bind(). Per farlo, posiziona il cursore sull'errore e premi Alt+Enter (Windows) o Option+Enter (Mac) sull'errore, come mostrato nello screenshot di seguito.

  1. All'interno della classe ViewHolder, all'interno della funzione bind(), assegna il listener di clic all'oggetto binding. Visualizzi un errore perché devi aggiornare l'oggetto di binding.
binding.clickListener = clickListener
  1. Per aggiornare il data binding, Pulisci e Ricompila il progetto. Potrebbe essere necessario invalidare anche le cache. Quindi, hai preso un listener di clic dal costruttore dell'adattatore e lo hai passato fino al view holder e all'oggetto di binding.

Passaggio 3: mostra un messaggio di notifica quando viene toccato un elemento

Ora hai il codice per acquisire un clic, ma non hai implementato cosa succede quando viene toccato un elemento dell'elenco. La risposta più semplice è visualizzare un messaggio di notifica che mostra nightId quando viene fatto clic su un elemento. In questo modo si verifica che quando viene fatto clic su un elemento dell'elenco, venga acquisito e trasmesso il nightId corretto.

  1. Apri SleepTrackerFragment.kt.
  2. In onCreateView(), trova la variabile adapter. Tieni presente che viene visualizzato un errore perché ora è previsto un parametro del listener di clic.
  3. Definisci un listener di clic passando un'espressione lambda a SleepNightAdapter. Questa semplice lambda mostra un toast con nightId, come mostrato di seguito. Devi importare Toast. Di seguito è riportata la definizione aggiornata completa.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Esegui l'app, tocca gli elementi e verifica che venga visualizzato un messaggio popup con il valore nightId corretto. Poiché gli elementi hanno valori nightId crescenti e l'app mostra prima la notte più recente, l'elemento con il valore nightId più basso si trova in fondo all'elenco.

In questa attività, modifichi il comportamento quando viene fatto clic su un elemento in RecyclerView, in modo che, anziché mostrare un messaggio di notifica, l'app passi a un fragment di dettagli che mostra ulteriori informazioni sulla notte selezionata.

Passaggio 1: navigare con un clic

In questo passaggio, anziché visualizzare solo un messaggio popup, modifichi l'espressione lambda del listener di clic in onCreateView() di SleepTrackerFragment per passare nightId a SleepTrackerViewModel e attivare la navigazione a SleepDetailFragment.

Definisci la funzione di gestione dei clic:

  1. Apri SleepTrackerViewModel.kt.
  2. All'interno di SleepTrackerViewModel, verso la fine, definisci la funzione di gestione dei clic onSleepNightClicked().
fun onSleepNightClicked(id: Long) {

}
  1. All'interno di onSleepNightClicked(), attiva la navigazione impostando _navigateToSleepDetail sul valore id della notte di sonno selezionata.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Implementa _navigateToSleepDetail. Come hai fatto in precedenza, definisci un private MutableLiveData per lo stato di navigazione. E un val pubblico da abbinare.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Definisci il metodo da chiamare al termine della navigazione dell'app. Chiamalo onSleepDetailNavigated() e imposta il valore su null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Aggiungi il codice per chiamare il gestore dei clic:

  1. Apri SleepTrackerFragment.kt e scorri verso il basso fino al codice che crea l'adattatore e definisce SleepNightListener per mostrare un messaggio di notifica.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Aggiungi il seguente codice sotto il messaggio di notifica per chiamare un gestore di clic, onSleepNighClicked(), in sleepTrackerViewModel quando viene toccato un elemento. Passa in nightId, in modo che il modello di visualizzazione sappia quale notte di sonno recuperare. Si verifica un errore perché non hai ancora definito onSleepNightClicked(). Puoi mantenere, commentare o eliminare il messaggio di notifica, a seconda delle tue preferenze.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Aggiungi il codice per osservare i clic:

  1. Apri SleepTrackerFragment.kt.
  2. In onCreateView(), subito sopra la dichiarazione di manager, aggiungi il codice per osservare il nuovo navigateToSleepDetail LiveData. Quando navigateToSleepDetail cambia, vai a SleepDetailFragment, passando per night, quindi chiama onSleepDetailNavigated(). Poiché l 'hai già fatto in un codelab precedente, ecco il codice:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Esegui il codice, fai clic su un elemento e… l'app si arresta in modo anomalo.

Gestisci i valori null negli adattatori di binding:

  1. Esegui di nuovo l'app in modalità di debug. Tocca un elemento e filtra i log per mostrare gli errori. Verrà visualizzata una traccia dello stack che include qualcosa di simile a quanto riportato di seguito.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

Purtroppo, l'analisi dello stack non indica chiaramente dove viene attivato questo errore. Uno svantaggio del data binding è che può rendere più difficile il debug del codice. L'app va in arresto anomalo quando fai clic su un elemento e l'unico nuovo codice è per la gestione del clic.

Tuttavia, si è scoperto che con questo nuovo meccanismo di gestione dei clic, ora è possibile chiamare gli adattatori di binding con un valore null per item. In particolare, all'avvio dell'app, LiveData inizia come null, quindi devi aggiungere controlli null a ciascun adattatore.

  1. In BindingUtils.kt, per ciascuno degli adattatori di binding, modifica il tipo dell'argomento item in modo che accetti valori nulli e racchiudi il corpo con item?.let{...}. Ad esempio, l'adattatore per sleepQualityString avrà questo aspetto. Modifica gli altri adattatori allo stesso modo.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Esegui l'app. Tocca un elemento e si apre una visualizzazione dettagliata.

Progetto Android Studio: RecyclerViewClickHandler.

Per fare in modo che gli elementi di un RecyclerView rispondano ai clic, collega i listener di clic agli elementi dell'elenco nel ViewHolder e gestisci i clic nel ViewModel.

Per fare in modo che gli elementi di un RecyclerView rispondano ai clic, devi:

  • Crea una classe di listener che accetta una lambda e la assegna a una funzione onClick().
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Imposta il listener dei clic sulla visualizzazione.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Passa il listener di clic al costruttore dell'adattatore, al view holder e aggiungilo all'oggetto di binding.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • Nel frammento che mostra la visualizzazione elenco, in cui crei l'adattatore, definisci un listener di clic passando un'espressione lambda all'adattatore.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Implementa il gestore dei clic nel modello di visualizzazione. Per i clic sugli elementi di elenco, in genere viene attivata la navigazione a un frammento di dettagli.

Corso Udacity:

Documentazione per sviluppatori Android:

Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:

  • Assegna i compiti, se richiesto.
  • Comunica agli studenti come inviare i compiti.
  • Valuta i compiti a casa.

Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.

Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

Supponiamo che la tua app contenga un RecyclerView che mostra gli articoli in una lista della spesa. La tua app definisce anche una classe di listener di clic:

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

Come rendi ShoppingListItemListener disponibile al data binding? Seleziona un'opzione.

▢ Nel file di layout che contiene RecyclerView che mostra la lista della spesa, aggiungi una variabile <data> per ShoppingListItemListener.

▢ Nel file di layout che definisce il layout per una singola riga nella lista della spesa, aggiungi una variabile <data> per ShoppingListItemListener.

▢ Nella classe ShoppingListItemListener, aggiungi una funzione per attivare l'associazione di dati:

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

▢ Nella classe ShoppingListItemListener, all'interno della funzione onClick(), aggiungi una chiamata per attivare l'associazione di dati:

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

Domanda 2

Dove aggiungi l'attributo android:onClick per fare in modo che gli elementi di un RecyclerView rispondano ai clic? Seleziona tutte le opzioni pertinenti.

▢ Nel file di layout che mostra RecyclerView, aggiungilo a <androidx.recyclerview.widget.RecyclerView>

▢ Aggiungilo al file di layout per un elemento nella riga. Se vuoi che l'intero elemento sia cliccabile, aggiungilo alla visualizzazione principale che contiene gli elementi nella riga.

▢ Aggiungilo al file di layout per un elemento nella riga. Se vuoi che un singolo TextView nell'elemento sia selezionabile, aggiungilo a <TextView>.

▢ Aggiungi sempre il file di layout per MainActivity.

Inizia la lezione successiva: 7.5: Intestazioni in RecyclerView