Android Kotlin Fundamentals 07.5: Headers in RecyclerView

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

In questo codelab, imparerai ad aggiungere un'intestazione che si estende per tutta la larghezza dell'elenco visualizzato in un RecyclerView. Ti basi sull'app per il monitoraggio del sonno dei codelab precedenti.

Cosa devi già sapere

  • Come creare un'interfaccia utente di base utilizzando un'attività, frammenti e visualizzazioni.
  • Come spostarsi tra i fragment e come 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 DAO e definire le entità.
  • Come utilizzare le coroutine per le interazioni con 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.
  • Come acquisire e gestire i clic sugli elementi in un RecyclerView.

Obiettivi didattici

  • Come utilizzare più di un ViewHolder con un RecyclerView per aggiungere elementi con un layout diverso. Nello specifico, come utilizzare un secondo ViewHolder per aggiungere un'intestazione sopra gli elementi visualizzati in RecyclerView.

In questo lab proverai a:

  • Sviluppa l'app TrackMySleepQuality del codelab precedente di questa serie.
  • Aggiungi un'intestazione che si estenda per tutta la larghezza dello schermo sopra le notti di sonno visualizzate in RecyclerView.

L'app di monitoraggio del sonno con cui inizi ha tre 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 al centro, consente di selezionare una valutazione della qualità del sonno. La terza schermata è una visualizzazione dettagliata che si apre quando l'utente tocca un elemento nella griglia.

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 un'intestazione alla griglia degli elementi visualizzati. La schermata principale finale sarà simile a questa:

Questo codelab insegna il principio generale di inclusione di elementi che utilizzano layout diversi in un RecyclerView. Un esempio comune è la presenza di intestazioni nell'elenco o nella griglia. Un elenco può avere una sola intestazione per descrivere i contenuti dell'elemento. Un elenco può anche avere più intestazioni per raggruppare e separare gli elementi in un unico elenco.

RecyclerView non sa nulla dei tuoi dati o del tipo di layout di ogni elemento. LayoutManager dispone gli elementi sullo schermo, ma l'adattatore adatta i dati da visualizzare e passa i segnaposto della visualizzazione a RecyclerView. Pertanto, aggiungerai il codice per creare le intestazioni nell'adattatore.

Due modi per aggiungere intestazioni

In RecyclerView, ogni elemento dell'elenco corrisponde a un numero di indice a partire da 0. Ad esempio:

[Actual Data] -> [Adapter Views]

[0: SleepNight] -> [0: SleepNight]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

Un modo per aggiungere intestazioni a un elenco è modificare l'adattatore in modo che utilizzi un ViewHolder diverso selezionando gli indici in cui deve essere visualizzata l'intestazione. Adapter sarà responsabile del monitoraggio dell'intestazione. Ad esempio, per mostrare un'intestazione nella parte superiore della tabella, devi restituire un ViewHolder diverso per l'intestazione durante la disposizione dell'elemento con indice zero. Quindi, tutti gli altri elementi verranno mappati con l'offset dell'intestazione, come mostrato di seguito.

[Actual Data] -> [Adapter Views]

[0: Header]

[0: SleepNight] -> [1: SleepNight]

[1: SleepNight] -> [2: SleepNight]

[2: SleepNight] -> [3: SleepNight.

Un altro modo per aggiungere intestazioni è modificare il set di dati di supporto della griglia di dati. Poiché tutti i dati da visualizzare sono memorizzati in un elenco, puoi modificare l'elenco in modo da includere elementi che rappresentino un'intestazione. Questo è un po' più semplice da capire, ma richiede di pensare a come progettare gli oggetti, in modo da poter combinare i diversi tipi di elementi in un unico elenco. Se implementato in questo modo, l'adattatore visualizzerà gli elementi che gli sono stati trasferiti. Quindi, l'elemento in posizione 0 è un'intestazione e l'elemento in posizione 1 è un SleepNight, che corrisponde direttamente a ciò che è sullo schermo.

[Actual Data] -> [Adapter Views]

[0: Header] -> [0: Header]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

[3: SleepNight] -> [3: SleepNight]

Ogni metodologia presenta vantaggi e svantaggi. La modifica del set di dati non introduce molte modifiche al resto del codice dell'adattatore e puoi aggiungere la logica dell'intestazione manipolando l'elenco dei dati. D'altra parte, l'utilizzo di un ViewHolder diverso selezionando gli indici per le intestazioni offre maggiore libertà nel layout dell'intestazione. Consente inoltre all'adattatore di gestire la modalità di adattamento dei dati alla visualizzazione senza modificare i dati di supporto.

In questo codelab, aggiorni RecyclerView per visualizzare un'intestazione all'inizio dell'elenco. In questo caso, l'app utilizzerà un ViewHolder diverso per l'intestazione rispetto agli elementi di dati. L'app controllerà l'indice dell'elenco per determinare quale ViewHolder utilizzare.

Passaggio 1: crea una classe DataItem

Per astrarre il tipo di elemento e consentire all'adattatore di gestire solo gli "elementi", puoi creare una classe di contenitore di dati che rappresenti un SleepNight o un Header. Il set di dati sarà quindi un elenco di elementi del titolare dei dati.

Puoi scaricare l'app iniziale da GitHub o continuare a utilizzare l'app SleepTracker che hai creato nel codelab precedente.

  1. Scarica il codice RecyclerViewHeaders-Starter da GitHub. La directory RecyclerViewHeaders-Starter contiene la versione iniziale dell'app SleepTracker necessaria per questo codelab. Se preferisci, puoi anche continuare con l'app completata del codelab precedente.
  2. Apri SleepNightAdapter.kt.
  3. Sotto la classe SleepNightListener, a livello superiore, definisci una classe sealed chiamata DataItem che rappresenta un elemento di dati.

    Una classe sealed definisce un tipo chiuso, il che significa che tutte le sottoclassi di DataItem devono essere definite in questo file. Di conseguenza, il numero di sottoclassi è noto al compilatore. Non è possibile che un'altra parte del codice definisca un nuovo tipo di DataItem che potrebbe danneggiare l'adattatore.
sealed class DataItem {

 }
  1. All'interno del corpo della classe DataItem, definisci due classi che rappresentano i diversi tipi di elementi di dati. Il primo è un SleepNightItem, un wrapper intorno a un SleepNight, quindi accetta un singolo valore denominato sleepNight. Per includerlo nel corso sigillato, estendilo fino al giorno DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. La seconda classe è Header, per rappresentare un'intestazione. Poiché un'intestazione non contiene dati effettivi, puoi dichiararla come object. Ciò significa che esisterà sempre e solo un'istanza di Header. Ancora una volta, fallo estendere DataItem.
object Header: DataItem()
  1. All'interno di DataItem, a livello di corso, definisci una proprietà abstract Long denominata id. Quando l'adattatore utilizza DiffUtil per determinare se e come un elemento è cambiato, DiffItemCallback deve conoscere l'ID di ogni elemento. Visualizzerai un errore perché SleepNightItem e Header devono eseguire l'override della proprietà astratta id.
abstract val id: Long
  1. In SleepNightItem, sostituisci id per restituire nightId.
override val id = sleepNight.nightId
  1. In Header, esegui l'override di id per restituire Long.MIN_VALUE, che è un numero molto, molto piccolo (letteralmente, -2 elevato alla potenza di 63). Pertanto, non entrerà mai in conflitto con alcun nightId esistente.
override val id = Long.MIN_VALUE
  1. Il codice finale dovrebbe avere questo aspetto e la tua app dovrebbe essere compilata senza errori.
sealed class DataItem {
    abstract val id: Long
    data class SleepNightItem(val sleepNight: SleepNight): DataItem()      {
        override val id = sleepNight.nightId
    }

    object Header: DataItem() {
        override val id = Long.MIN_VALUE
    }
}

Passaggio 2: crea un ViewHolder per l'intestazione

  1. Crea il layout per l'intestazione in un nuovo file di risorse di layout denominato header.xml che visualizza un TextView. Non c'è niente di entusiasmante in questo, quindi ecco il codice.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Sleep Results"
    android:padding="8dp" />
  1. Estrai "Sleep Results" in una risorsa stringa e chiamala header_text.
<string name="header_text">Sleep Results</string>
  1. In SleepNightAdapter.kt, all'interno di SleepNightAdapter, sopra la classe ViewHolder, crea una nuova classe TextViewHolder. Questa classe gonfia il layout textview.xml e restituisce un'istanza TextViewHolder. Poiché l'hai già fatto, ecco il codice e dovrai importare View e R:
    class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): TextViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = layoutInflater.inflate(R.layout.header, parent, false)
                return TextViewHolder(view)
            }
        }
    }

Passaggio 3: aggiorna SleepNightAdapter

Dopodiché devi aggiornare la dichiarazione di SleepNightAdapter. Anziché supportare un solo tipo di ViewHolder, deve essere in grado di utilizzare qualsiasi tipo di segnaposto della visualizzazione.

Definisci i tipi di elementi

  1. In SleepNightAdapter.kt, a livello superiore, sotto le istruzioni import e sopra SleepNightAdapter, definisci due costanti per i tipi di visualizzazione.

    Il RecyclerView dovrà distinguere il tipo di visualizzazione di ogni elemento, in modo da poter assegnare correttamente un segnaposto di visualizzazione.
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. All'interno di SleepNightAdapter, crea una funzione per eseguire l'override di getItemViewType() in modo da restituire la costante di intestazione o elemento corretta a seconda del tipo dell'elemento corrente.
override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
            is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
        }
    }

Aggiorna la definizione di SleepNightAdapter

  1. Nella definizione di SleepNightAdapter, aggiorna il primo argomento per ListAdapter da SleepNight a DataItem.
  2. Nella definizione di SleepNightAdapter, modifica il secondo argomento generico per ListAdapter da SleepNightAdapter.ViewHolder a RecyclerView.ViewHolder. Vedrai alcuni errori relativi agli aggiornamenti necessari e l'intestazione del corso dovrebbe avere l'aspetto mostrato di seguito.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Aggiorna onCreateViewHolder()

  1. Modifica la firma di onCreateViewHolder() per restituire un RecyclerView.ViewHolder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. Espandi l'implementazione del metodo onCreateViewHolder() per testare e restituire il view holder appropriato per ogni tipo di elemento. Il metodo aggiornato dovrebbe essere simile al codice riportato di seguito.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
            ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
            else -> throw ClassCastException("Unknown viewType ${viewType}")
        }
    }

Aggiorna onBindViewHolder()

  1. Modifica il tipo di parametro di onBindViewHolder() da ViewHolder a RecyclerView.ViewHolder.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. Aggiungi una condizione per assegnare i dati al titolare della visualizzazione solo se è un ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Esegui il cast del tipo di oggetto restituito da getItem() a DataItem.SleepNightItem. La funzione onBindViewHolder() completata dovrebbe avere il seguente aspetto.
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

Aggiorna i callback di DiffUtil

  1. Modifica i metodi in SleepNightDiffCallback per utilizzare la nuova classe DataItem anziché SleepNight. Elimina l'avviso di lint come mostrato nel codice seguente.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
    override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem.id == newItem.id
    }
    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem == newItem
    }
}

Aggiungere e inviare l'intestazione

  1. All'interno di SleepNightAdapter, sotto onCreateViewHolder(), definisci una funzione addHeaderAndSubmitList() come mostrato di seguito. Questa funzione accetta un elenco di SleepNight. Anziché utilizzare submitList(), fornito da ListAdapter, per inviare l'elenco, utilizzerai questa funzione per aggiungere un'intestazione e poi inviare l'elenco.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. All'interno di addHeaderAndSubmitList(), se l'elenco passato è null, restituisci solo un'intestazione, altrimenti aggiungi l'intestazione all'inizio dell'elenco e poi invia l'elenco.
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. Apri SleepTrackerFragment.kt e modifica la chiamata a submitList() in addHeaderAndSubmitList().
  1. Esegui l'app e osserva come viene visualizzata l'intestazione come primo elemento nell'elenco degli elementi del sonno.

Per questa app è necessario risolvere due problemi. Uno è visibile, l'altro no.

  • L'intestazione viene visualizzata nell'angolo in alto a sinistra e non è facilmente distinguibile.
  • Non è molto importante per un breve elenco con un'intestazione, ma non devi manipolare gli elenchi in addHeaderAndSubmitList() nel thread UI. Immagina un elenco con centinaia di elementi, più intestazioni e una logica per decidere dove inserire gli elementi. Questo lavoro appartiene a una coroutine.

Modifica addHeaderAndSubmitList() per utilizzare le coroutine:

  1. Al livello superiore all'interno della classe SleepNightAdapter, definisci un CoroutineScope con Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. In addHeaderAndSubmitList(), avvia una coroutine in adapterScope per manipolare l'elenco. Poi passa al contesto Dispatchers.Main per inviare l'elenco, come mostrato nel codice riportato di seguito.
 fun addHeaderAndSubmitList(list: List<SleepNight>?) {
        adapterScope.launch {
            val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
            withContext(Dispatchers.Main) {
                submitList(items)
            }
        }
    }
  1. Il codice deve essere compilato ed eseguito e non noterai alcuna differenza.

Al momento, l'intestazione ha la stessa larghezza degli altri elementi della griglia, occupando uno spazio orizzontale e verticale. L'intera griglia contiene tre elementi di una larghezza di colonna, quindi l'intestazione deve utilizzare tre colonne orizzontalmente.

Per correggere la larghezza dell'intestazione, devi indicare a GridLayoutManager quando estendere i dati a tutte le colonne. Per farlo, configura SpanSizeLookup su un GridLayoutManager. Si tratta di un oggetto di configurazione che GridLayoutManager utilizza per determinare il numero di intervalli da utilizzare per ogni elemento dell'elenco.

  1. Apri SleepTrackerFragment.kt.
  2. Trova il codice in cui definisci manager, verso la fine di onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Sotto manager, definisci manager.spanSizeLookup, come mostrato. Devi creare un object perché setSpanSizeLookup non accetta un lambda. Per creare un object in Kotlin, digita object : classname, in questo caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Potresti ricevere un errore del compilatore per chiamare il costruttore. In questo caso, apri il menu Intenzione con Option+Enter (Mac) o Alt+Enter (Windows) per applicare la chiamata al costruttore.
  1. Poi riceverai un errore su object che ti dice che devi eseguire l'override dei metodi. Posiziona il cursore su object, premi Option+Enter (Mac) o Alt+Enter (Windows) per aprire il menu degli intent, quindi esegui l'override del metodo getSpanSize().
  1. Nel corpo di getSpanSize(), restituisci la dimensione dello span corretta per ogni posizione. La posizione 0 ha una dimensione di intervallo di 3, mentre le altre posizioni hanno una dimensione di intervallo di 1. Il codice completato dovrebbe avere l'aspetto seguente:
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  1. Per migliorare l'aspetto dell'intestazione, apri header.xml e aggiungi questo codice al file di layout header.xml.
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
  1. Esegui l'app. Dovrebbe avere un aspetto simile a quello dello screenshot seguente.

Complimenti! Ecco fatto.

Progetto Android Studio: RecyclerViewHeaders

  • Un'intestazione è in genere un elemento che si estende per tutta la larghezza di un elenco e funge da titolo o separatore. Un elenco può avere una sola intestazione per descrivere i contenuti degli elementi o più intestazioni per raggruppare gli elementi e separarli tra loro.
  • Un RecyclerView può utilizzare più segnaposto di visualizzazione per ospitare un insieme eterogeneo di elementi, ad esempio intestazioni ed elementi di elenco.
  • Un modo per aggiungere le intestazioni è modificare l'adattatore in modo che utilizzi un ViewHolder diverso selezionando gli indici in cui deve essere visualizzata l'intestazione. Adapter è responsabile del monitoraggio dell'intestazione.
  • Un altro modo per aggiungere intestazioni è modificare il set di dati di supporto (l'elenco) per la griglia di dati, come hai fatto in questo codelab.

Di seguito sono riportati i passaggi principali per aggiungere un'intestazione:

  • Estrai i dati nell'elenco creando un DataItem che può contenere un'intestazione o dati.
  • Crea un segnaposto della visualizzazione con un layout per l'intestazione nell'adattatore.
  • Aggiorna l'adattatore e i relativi metodi per utilizzare qualsiasi tipo di RecyclerView.ViewHolder.
  • In onCreateViewHolder(), restituisci il tipo corretto di segnaposto della visualizzazione per l'elemento di dati.
  • Aggiorna SleepNightDiffCallback per funzionare con la classe DataItem.
  • Crea una funzione addHeaderAndSubmitList() che utilizza le coroutine per aggiungere l'intestazione al set di dati e poi chiama submitList().
  • Implementa GridLayoutManager.SpanSizeLookup() per fare in modo che l'intestazione abbia una larghezza di tre colonne.

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

Quale delle seguenti affermazioni relative a ViewHolder è vera?

▢ Un adattatore può utilizzare più classi ViewHolder per contenere intestazioni e vari tipi di dati.

▢ Puoi avere esattamente un segnaposto per i dati e uno per un'intestazione.

▢ Un RecyclerView supporta più tipi di intestazioni, ma i dati devono essere uniformi.

▢ Quando aggiungi un'intestazione, crei una sottoclasse di RecyclerView per inserirla nella posizione corretta.

Domanda 2

Quando conviene utilizzare le coroutine con un RecyclerView? Seleziona tutte le affermazioni vere.

▢ Mai. Un RecyclerView è un elemento UI e non deve utilizzare coroutine.

▢ Utilizza le coroutine per le attività di lunga durata che potrebbero rallentare la UI.

▢ Le manipolazioni degli elenchi possono richiedere molto tempo e devono sempre essere eseguite utilizzando le coroutine.

▢ Utilizza le coroutine con le funzioni di sospensione per evitare di bloccare il thread principale.

Domanda 3

Quale delle seguenti operazioni NON devi eseguire quando utilizzi più di un ViewHolder?

▢ In ViewHolder, fornisci più file di layout da gonfiare in base alle esigenze.

▢ In onCreateViewHolder(), restituisci il tipo corretto di view holder per l'elemento di dati.

▢ In onBindViewHolder(), associa i dati solo se il segnaposto della visualizzazione è il tipo corretto di segnaposto per l'elemento di dati.

▢ Generalizza la firma della classe dell'adattatore per accettare qualsiasi RecyclerView.ViewHolder.

Inizia la lezione successiva: 8.1 Recuperare dati da internet

Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab di Android Kotlin Fundamentals.