Android Kotlin Fundamentals 07.4: interazione con gli elementi di RecyclerView

Questo codelab fa parte del corso Android Kotlin Fundamentals. Otterrai il massimo valore da questo corso se lavori in sequenza nei codelab. Tutti i codelab del corso sono elencati nella pagina di destinazione di Android Kotlin Fundamentals.

Introduzione

La maggior parte delle app che utilizza elenchi e griglie che mostrano elementi consente agli utenti di interagire con tali elementi. Toccare un elemento da un elenco e visualizzare i dettagli dell'articolo è un caso d'uso molto comune per questo tipo di interazione. A tal fine, puoi aggiungere dei listener di clic che rispondono ai tocchi degli utenti su alcuni elementi mostrando una visualizzazione dettagliata.

In questo codelab, aggiungi l'interazione con il tuo RecyclerView, a partire da una versione estesa dell'app di monitoraggio del sonno della precedente serie di codelab.

Informazioni importanti

  • Creare un'interfaccia utente di base con attività, frammenti e viste.
  • Spostamento tra frammenti e utilizzo di safeArgs per trasmettere dati tra frammenti.
  • Visualizza modelli, fabbriche di modelli, trasformazioni e LiveData e i loro osservatori.
  • Come creare un database Room, creare un oggetto Data Access (DAO) e definire le entità.
  • Come utilizzare le coroutine per i database e altre attività di lunga durata.
  • Come implementare un elemento RecyclerView di base con un layout Adapter, ViewHolder e un elemento.
  • Come implementare l'associazione di dati per RecyclerView.
  • Come creare e utilizzare adattatori di associazione per trasformare i dati.
  • Come utilizzare GridLayoutManager.

Obiettivi didattici

  • Come rendere selezionabili gli elementi in RecyclerView. Implementare un listener di clic per accedere a una visualizzazione dei dettagli quando viene fatto clic su un elemento.

In questo lab proverai a:

  • Crea su una versione estesa dell'app TrackMySleepQualità del codelab precedente di questa serie.
  • Aggiungi un listener di clic al tuo elenco e inizia ad ascoltare l'interazione dell'utente. Quando un elemento dell'elenco viene toccato, viene attivata la navigazione verso un frammento con i dettagli dell'elemento selezionato. Il codice di avvio fornisce il codice per il frammento di dettaglio e il codice di navigazione.

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

La prima schermata, mostrata a sinistra, contiene pulsanti per avviare e interrompere il monitoraggio. Sullo schermo sono visualizzati alcuni dati del sonno dell'utente. Il pulsante Cancella elimina definitivamente tutti i dati raccolti dall'app per l'utente. La seconda schermata, mostrata a destra, consente di selezionare una valutazione della qualità del sonno.

Questa app utilizza un'architettura semplificata con un controller UI, un modello vista e LiveData, oltre a un database Room per conservare i dati relativi al sonno.

In questo codelab, puoi aggiungere la possibilità di rispondere quando un utente tocca un elemento della griglia e viene visualizzata 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 RecyclerViewClickHandler-Starter 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 utilizzarla dal codelab precedente

Se stai lavorando dall'app iniziale fornita in GitHub per questo codelab, vai al passaggio successivo.

Se vuoi continuare a utilizzare la tua app di monitoraggio del sonno creata nel codelab precedente, segui le istruzioni riportate di seguito per aggiornare l'app esistente in modo che abbia il codice per il frammento della schermata dei dettagli.

  1. Anche se continui a utilizzare l'app esistente, recupera il codice RecyclerViewClickHandler-Starter di GitHub in modo da poter copiare i file.
  2. Copia tutti i file nel pacchetto sleepdetail.
  3. Copia il file fragment_sleep_detail.xml nella cartella layout.
  4. Copia i contenuti aggiornati di navigation.xml, in modo da aggiungere 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: controlla il codice per la schermata dei dettagli del sonno

In questo codelab, implementi un gestore dei clic che passa a un frammento che mostra i dettagli della notte di sonno selezionata. Il tuo codice di avvio contiene già il frammento e il grafico di navigazione per questo SleepDetailFragment, perché è un po' di codice e i frammenti e la navigazione non fanno parte di questo codelab. Acquisisci familiarità con il seguente codice:

  1. Nell'app, trova il pacchetto sleepdetail. Questo pacchetto contiene il frammento, il modello di visualizzazione e la fabbrica dei modelli di visualizzazione per un frammento che mostra i dettagli relativi a una notte di sonno.

  2. Nel pacchetto sleepdetail, apri e analizza il codice per il SleepDetailViewModel. Questo modello di vista assume la chiave per un SleepNight e un DAO nel costruttore.

    Il corpo del corso contiene codice per recuperare l'elemento SleepNight per la chiave specificata e la variabile navigateToSleepTracker per controllare la navigazione fino all'elemento SleepTrackerFragment quando viene premuto il pulsante Chiudi.

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

  3. Nel pacchetto sleepdetail, apri e analizza il codice per il SleepDetailFragment. Noterai la configurazione dell'associazione di dati, il modello di visualizzazione e l'osservatore per la navigazione.

  4. Nel pacchetto sleepdetail, apri e ispeziona il codice di SleepDetailViewModelFactory.

  5. Nella cartella del layout, controlla fragment_sleep_detail.xml. Osserva la variabile sleepDetailViewModel definita nel tag <data> per ottenere i dati da visualizzare in ogni vista dal modello della vista.

    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 di dettaglio.

  6. Apri il file navigation.xml. Per la 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 tracker del sonno alla schermata dei dettagli.

In questa attività, dovrai aggiornare RecyclerView in modo che risponda ai tocchi degli utenti mostrando una schermata dei dettagli relativi all'elemento toccato.

Ricevere i clic e gestirli è un'attività composta da due parti: per prima cosa, devi ascoltare e ricevere il clic e determinare su quale elemento è stato fatto clic. Devi poi rispondere al clic con un'azione.

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

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

ViewHolder è la scelta migliore per ascoltare i clic, ma in genere non è il posto giusto per gestirli. Qual è quindi il miglior modo per gestire i clic?

  • Adapter mostra gli elementi di dati nelle viste, per consentirti di gestire i clic nell'adattatore. Tuttavia, il compito dell'adattatore è adattare i dati alla Rete Display, non alla logica dell'app.
  • Solitamente devi gestire i clic 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 attivalo dal layout degli elementi

  1. Nella cartella sleeptracker, apri SleepNightAdapter.kt.
  2. Alla fine del file, crea una nuova classe del listener, SleepNightListener, al livello superiore.
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 chiamata chiama questa funzione onClick(). La proprietà android:onClick verrà impostata in seguito su questa funzione.
class SleepNightListener() {
    fun onClick() = 
}
  1. Aggiungi un argomento di funzione night di tipo SleepNight a onClick(). La vista sa quale elemento sta visualizzando e quali informazioni devono essere trasmesse per gestire il clic.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Per definire le operazioni di onClick(), fornisci un callback clickListener nel costruttore di SleepNightListener e assegnalo a onClick().

    Assegnare il lambda che gestisce il nome con un nome, clickListener, è utile per tenerne traccia nel corso della lezione. Il callback clickListener necessita solo di night.nightId per accedere ai dati dal database. Il corso completato di SleepNightListener dovrebbe essere simile al seguente codice.
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 l'associazione di dati. Assegna al nuovo <variable> un name dell'elemento clickListener. Imposta il valore 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 ascoltare 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 per l'associazione di dati, come mostrato di seguito:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Passaggio 2: passa il listener di clic al proprietario della vista e all'oggetto dell'associazione

  1. Apri SleepNightAdapter.kt.
  2. Modifica il costruttore della classe SleepNightAdapter per ricevere un val clickListener: SleepNightListener. Quando l'adattatore vincola ViewHolder, dovrà fornirlo con il listener di clic.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. In onBindViewHolder(), aggiorna la chiamata a holder.bind() in modo che passi anche il listener di clic a ViewHolder. Verrà visualizzato un errore del compilatore perché hai aggiunto un parametro alla chiamata 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. Viene visualizzato un errore perché devi aggiornare l'oggetto associazione.
binding.clickListener = clickListener
  1. Per aggiornare l'associazione di dati, pulisci e ricrea il progetto. Potresti dover invalidare anche le cache. Quindi, hai preso un listener di clic dal costruttore di adattatori e li hai trasmessi fino al proprietario della vista e all'oggetto di associazione.

Passaggio 3: viene visualizzato un toast 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 toast che mostra nightId quando viene fatto clic su un elemento. In questo modo, quando viene fatto clic su un elemento dell'elenco, viene acquisita e trasmessa l'elemento nightId corretto.

  1. Apri SleepTrackerFragment.kt.
  2. In onCreateView(), trova la variabile adapter. Tieni presente che viene mostrato un errore perché ora è previsto un parametro listener di clic.
  3. Definisci un listener di clic passando un lambda al SleepNightAdapter. Questo semplice lambda mostra semplicemente un toast con l'icona nightId, come mostrato di seguito. Dovrai 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 mostrino un toast con l'elemento nightId corretto. Poiché gli elementi hanno valori nightId in aumento e l'app mostra per prima cosa la notte più recente, l'elemento con il nightId più basso si trova in fondo all'elenco.

In questa attività, cambi il comportamento quando viene fatto clic su un elemento in RecyclerView; in questo modo, invece di mostrare un toast, l'app passa a un frammento di dettaglio che mostra ulteriori informazioni sulla notte selezionata.

Passaggio 1. Naviga al clic

In questo passaggio, invece di visualizzare solo un toast, devi modificare il lambda del listener di clic in onCreateView() di SleepTrackerFragment per passare il nightId a SleepTrackerViewModel e attivare la navigazione per SleepDetailFragment.

Definisci la funzione di gestore dei clic:

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

}
  1. All'interno di onSleepNightClicked(), attiva la navigazione impostando _navigateToSleepDetail sul valore superato di id della notte di sonno selezionata.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Implementa _navigateToSleepDetail. Come hai già fatto in precedenza, definisci un private MutableLiveData per lo stato di navigazione. E anche una risorsa pubblica val da associare.
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 relativo 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 toast.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Aggiungi il seguente codice sotto il toast per chiamare un gestore dei clic, onSleepNighClicked(), in sleepTrackerViewModel quando viene toccato un elemento. Passa il nightId, in modo che il modello di vista sappia quale notte dormire. Questo genera un errore perché non hai ancora definito onSleepNightClicked(). Puoi tenere, commentare o eliminare il toast come preferisci.
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 alla SleepDetailFragment, passa alla night, quindi chiama onSleepDetailNavigated() dopo. Poiché hai eseguito questa operazione in precedenza 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 associazione:

  1. Esegui di nuovo l'app in modalità di debug. Tocca un elemento e filtra i log per mostrare Errori. Verrà mostrata un'analisi dello stack che include informazioni simili a quelle che seguono.
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 rende evidente dove viene attivato questo errore. Uno svantaggio dell'associazione di dati è che può rendere più difficile eseguire il debug del codice. L'app si arresta in modo anomalo quando fai clic su un elemento e l'unico nuovo codice viene gestito per il clic.

Tuttavia, con il nuovo meccanismo di gestione dei clic ora è possibile chiamare gli adattatori di associazione con un valore null per item. In particolare, all'avvio dell'app, LiveData inizia come null, pertanto devi aggiungere controlli null a ogni adattatore.

  1. In BindingUtils.kt, per ciascuno degli adattatori di associazione, modifica il tipo di argomento item in zeroble e aggrega il corpo con item?.let{...}. Ad esempio, l'adattatore per sleepQualityString sarà simile a questo. Cambia nello stesso modo anche gli altri adattatori.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Esegui l'app. Tocca un elemento per aprire una visualizzazione dei dettagli.

Progetto Android Studio: RecyclerViewClickHandler.

Per fare in modo che gli elementi di un RecyclerView rispondano ai clic, allega i listener di clic per elencare gli elementi in ViewHolder e gestisci i clic in ViewModel.

Per fare in modo che gli elementi di un elemento RecyclerView rispondano ai clic, è necessario:

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

Corso Udacity:

Documentazione per gli sviluppatori Android:

In questa sezione sono elencati i possibili compiti per gli studenti che lavorano attraverso questo codelab nell'ambito di un corso tenuto da un insegnante. Spetta all'insegnante fare quanto segue:

  • Assegna i compiti, se necessario.
  • Comunica agli studenti come inviare compiti.
  • Valuta i compiti.

Gli insegnanti possono utilizzare i suggerimenti solo quanto e come vogliono e dovrebbero assegnare i compiti che ritengono appropriati.

Se stai lavorando da solo a questo codelab, puoi utilizzare questi compiti per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

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

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

Come rendi disponibile ShoppingListItemListener per l'associazione di dati? Seleziona un'opzione.

▢ nel file di layout contenente l'elemento RecyclerView che mostra la lista della spesa, aggiungi una variabile <data> per l'attributo ShoppingListItemListener.

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

▢ nel corso ShoppingListItemListener, aggiungi una funzione per abilitare 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 far sì che gli elementi di un elemento RecyclerView rispondano ai clic? Seleziona tutte le risposte pertinenti.

▢ nel file di layout in cui è visualizzato RecyclerView, che puoi aggiungere a <androidx.recyclerview.widget.RecyclerView>

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

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

▢ Aggiungi sempre il file di layout per MainActivity.

Inizia la lezione successiva: 7.5: intestazioni in RecyclerView