Android Kotlin Fundamentals 07.5: intestazioni in 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

In questo codelab, imparerai ad aggiungere un'intestazione che estende tutta la larghezza dell'elenco visualizzato in RecyclerView. Si basa sull'app del tracker del sonno dei codelab precedenti.

Informazioni importanti

  • Come creare un'interfaccia utente di base con attività, frammenti e viste.
  • Come navigare tra i frammenti e come utilizzare safeArgs per trasmettere dati tra i frammenti.
  • Visualizza modelli, fabbriche di modelli, trasformazioni e LiveData e i loro osservatori.
  • Come creare un database Room, creare un DAO e definire le entità.
  • Come utilizzare le coroutine per le interazioni con 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.
  • Come acquisire e gestire i clic sugli elementi in un RecyclerView.

Obiettivi didattici

  • Come utilizzare più elementi ViewHolder con 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:

  • Crea sull'app TrackMySleepQualità del codelab precedente di questa serie.
  • Aggiungi un'intestazione che si estende per tutta la larghezza dello schermo sopra le notti di sonno visualizzate in RecyclerView.

L'app di monitoraggio del sonno con tre 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. Il secondo schermo, mostrato al centro, serve per selezionare un punteggio della qualità del sonno. La terza schermata è una visualizzazione dei dettagli che si apre quando l'utente tocca un elemento della griglia.

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, aggiungi un'intestazione alla griglia di elementi visualizzati. La schermata principale finale sarà simile alla seguente:

Questo codelab insegna il principio generale dell'inclusione di elementi che utilizzano layout diversi in un elemento RecyclerView. Un esempio comune è avere 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 articoli in un unico elenco.

RecyclerView non ha informazioni sui tuoi dati o sul tipo di layout di ciascun elemento. L'elemento LayoutManager dispone gli elementi sullo schermo, ma l'adattatore adatta i dati per la visualizzazione e passa i titolari dell'inquadratura a RecyclerView. Quindi, aggiungi il codice per creare intestazioni nell'adattatore.

Due modi per aggiungere le intestazioni

In RecyclerView, ogni voce dell'elenco corrisponde a un numero di indice che inizia da 0. Ad esempio:

[Dati effettivi] -> [Visualizzazioni dell'adattatore]

[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 diverso ViewHolder controllando gli indici in cui deve essere mostrata l'intestazione. Adapter sarà responsabile di tenere traccia dell'intestazione. Ad esempio, per mostrare un'intestazione nella parte superiore della tabella, devi restituire un attributo ViewHolder diverso per l'intestazione quando definisci l'elemento con valore zero. Successivamente, tutti gli altri elementi saranno mappati con l'offset dell'intestazione, come mostrato di seguito.

[Dati effettivi] -> [Visualizzazioni dell'adattatore]

[0: Intestazione]

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

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

[2: SleepNight] -> [3: Notte del sonno.

Un altro modo per aggiungere intestazioni è modificare il set di dati di supporto per la griglia dei dati. Poiché tutti i dati da visualizzare sono archiviati in un elenco, puoi modificarlo in modo da includere elementi che rappresentino un'intestazione. È 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. In questo modo, l'adattatore mostrerà gli elementi trasmessi. Pertanto, l'elemento in posizione 0 è un'intestazione e l'elemento in posizione 1 è un SleepNight, che viene mappato direttamente a ciò che è sullo schermo.

[Dati effettivi] -> [Visualizzazioni dell'adattatore]

[0: Intestazione] -> [0: Intestazione]

[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 apporta molte modifiche al resto del codice dell'adattatore e puoi aggiungere una logica di intestazione manipolando l'elenco di dati. D'altra parte, l'utilizzo di un ViewHolder diverso controllando gli indici per le intestazioni offre maggiore libertà sul layout dell'intestazione. Inoltre, consente all'adattatore di gestire il modo in cui i dati vengono adattati alla vista senza modificare i dati di supporto.

In questo codelab, dovrai aggiornare RecyclerView in modo che mostri un'intestazione all'inizio dell'elenco. In questo caso, l'app utilizzerà un valore ViewHolder diverso per l'intestazione rispetto a quello per gli 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 "items" puoi creare una classe di titolare dei dati che rappresenta un SleepNight o un Header. Il set di dati diventerà un elenco di elementi del titolare dei dati.

Puoi scaricare l'app iniziale da GitHub o continuare con 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 terminata dal codelab precedente.
  2. Apri SleepNightAdapter.kt.
  3. Sotto la classe SleepNightListener, nel livello superiore, definisci una classe sealed denominata 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 per un'altra parte del codice definire un nuovo tipo di DataItem che potrebbe danneggiare l'adattatore.
sealed class DataItem {

 }
  1. Nel corpo della classe DataItem, definisci due classi che rappresentano i diversi tipi di elementi di dati. Il primo è un SleepNightItem, che è un wrapper attorno a un SleepNight, quindi richiede un unico valore chiamato sleepNight. Per farne parte nella classe chiusa, fai in modo che l'estensione venga estesa a 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 sarà presente una sola istanza di Header. Ribadisci che deve estendere DataItem.
object Header: DataItem()
  1. All'interno di DataItem, a livello di classe, 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 ciascun elemento. Verrà visualizzato un errore, poiché SleepNightItem e Header devono sostituire la 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 (ossia, -2 alla potenza di 63). Pertanto, non ci saranno conflitti con nessun nightId esistente.
override val id = Long.MIN_VALUE
  1. Il codice completato dovrebbe essere simile all'esempio seguente e la tua app dovrebbe essere creata 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 chiamato header.xml che mostra un elemento TextView. Non c'è niente di interessante in questo contesto, 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 chiamalo 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 aumenta il layout textview.xml e restituisce un'istanza TextViewHolder. Poiché hai eseguito questa operazione in precedenza, ecco il codice che 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. Invece di supportare un solo tipo di ViewHolder, deve essere in grado di utilizzare qualsiasi tipo di titolare della vista.

Tipi di elementi

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

    RecyclerView dovrà distinguere ogni tipo di vista degli elementi, in modo da poterli assegnare correttamente a un titolare della vista.
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. All'interno di SleepNightAdapter, crea una funzione che sostituisca getItemViewType() per restituire la costante intestazione o elemento corretta, a seconda del tipo di 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 essere simile a quella mostrata di seguito.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Aggiornamento 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 relativo titolare per ogni tipo di elemento. Il metodo aggiornato dovrebbe essere simile al seguente codice.
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}")
        }
    }

Aggiornamento onBindViewHolder()

  1. Cambia 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 dati al titolare della vista solo se quest'ultimo è un ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Trasmetti il tipo di oggetto restituito da getItem() a DataItem.SleepNightItem. La funzione onBindViewHolder() completata dovrebbe essere simile a questa.
  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 il callback diffUtil

  1. Modifica i metodi in SleepNightDiffCallback per utilizzare la nuova classe DataItem anziché SleepNight. Elimina l'avviso di lint come mostrato nel codice riportato di seguito.
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
    }
}

Aggiungi e invia l'intestazione

  1. All'interno di SleepNightAdapter, sotto onCreateViewHolder(), definisci una funzione addHeaderAndSubmitList() come mostrato di seguito. Questa funzione richiede un elenco di SleepNight. Invece di utilizzare submitList(), fornito da ListAdapter, per inviare l'elenco, dovrai utilizzare 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 allega 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 cambia la chiamata in submitList() in addHeaderAndSubmitList().
  1. Esegui l'app e controlla come viene visualizzata l'intestazione come primo elemento dell'elenco di elementi del sonno.

È necessario risolvere due problemi per questa app. Uno è visibile e l'altro no.

  • L'intestazione viene visualizzata nell'angolo in alto a sinistra e non è facile da distinguere.
  • Non è importante per un breve elenco con un'intestazione, ma non devi eseguire la manipolazione dell'elenco in addHeaderAndSubmitList() sul thread dell'interfaccia utente. Immagina un elenco con centinaia di articoli, più intestazioni e logica per decidere dove inserire gli articoli. Quest'opera appartiene a una coroutine.

Modifica addHeaderAndSubmitList() per utilizzare le coroutine:

  1. Al livello superiore della classe SleepNightAdapter, definisci un elemento CoroutineScope con Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. In addHeaderAndSubmitList(), avvia una coroutine nel dispositivo adapterScope per manipolare l'elenco. Passa quindi al contesto Dispatchers.Main per inviare l'elenco, come mostrato nel codice seguente.
 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 dovrebbe essere creato ed eseguito e non noterai alcuna differenza.

Attualmente, l'intestazione ha la stessa larghezza degli altri elementi della griglia e occupa un intervallo orizzontalmente e in verticale. L'intera griglia si adatta orizzontalmente a tre elementi di una larghezza, quindi l'intestazione dovrebbe utilizzare tre sezioni in orizzontale.

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

  1. Apri SleepTrackerFragment.kt.
  2. Individua il codice verso cui definisci manager, verso la fine di onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Sotto manager, definisci manager.spanSizeLookup, come mostrato. Devi preparare un object perché setSpanSizeLookup non prende una lambda. Per creare un elemento object in Kotlin, digita object : classname, in questo caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Potresti ricevere un errore di compilazione per chiamare il costruttore. In questo caso, apri il menu dell'intent con Option+Enter (Mac) o Alt+Enter (Windows) per applicare la chiamata costruttore.
  1. Dopodiché riceverai un errore su object che ti informa che devi sostituire i metodi. Posiziona il cursore su object, premi Option+Enter (Mac) o Alt+Enter (Windows) per aprire il menu delle intenzioni, quindi sostituisci il metodo getSpanSize().
  1. Nel corpo di getSpanSize(), restituisci le dimensioni corrette della sezione per ogni posizione. La posizione 0 ha un intervallo di 3, mentre le altre posizioni hanno una dimensione di 1. Il codice completato sarà simile al seguente codice:
    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 la tua app. Dovrebbe essere simile allo screenshot di seguito.

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 un'unica intestazione per descrivere i contenuti degli elementi oppure più intestazioni per raggruppare gli elementi e separarli l'uno dall'altro.
  • Un RecyclerView può utilizzare più sistemi di visualizzazione per includere un insieme eterogeneo di elementi, ad esempio intestazioni ed elementi di elenco.
  • Un modo per aggiungere intestazioni è modificare l'adattatore in modo che utilizzi un altro ViewHolder controllando gli indici in cui l'intestazione deve essere visualizzata. Adapter è responsabile del monitoraggio dell'intestazione.
  • Un altro modo per aggiungere intestazioni è modificare il set di dati di supporto (l'elenco) per la griglia dei dati.

Ecco i passaggi principali per l'aggiunta di un'intestazione:

  • Estrai i dati nel tuo elenco creando un elemento DataItem che può contenere un'intestazione o dei dati.
  • Crea un supporto 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 di visualizzazione corretto per l'elemento di dati.
  • Aggiorna SleepNightDiffCallback per utilizzare il corso DataItem.
  • Crea una funzione addHeaderAndSubmitList() che utilizzi le coroutine per aggiungere l'intestazione al set di dati, quindi chiami submitList().
  • Implementa GridLayoutManager.SpanSizeLookup() in modo che solo l'intestazione abbia tre sezioni.

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

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 un solo titolare della vista per i dati e uno per l'intestazione.

▢ A RecyclerView supporta più tipi di intestazioni, ma i dati devono essere coerenti.

▢ Quando aggiungi un'intestazione, esegui la sottoclasse RecyclerView per inserire l'intestazione nella posizione corretta.

Domanda 2

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

▢ Mai. RecyclerView è un elemento dell'interfaccia utente che non deve utilizzare coroutine.

▢ Usa coroutine per attività a lunga esecuzione che potrebbero rallentare l'interfaccia utente.

▢ Le manipolazioni degli elenchi possono richiedere molto tempo ed è consigliabile farlo sempre utilizzando coroutine.

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

Domanda 3

Quali delle seguenti operazioni NON devi fare quando utilizzi più di un ViewHolder?

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

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

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

▢ In generale, firma la classe dell'adattatore per accettare qualsiasi RecyclerView.ViewHolder.

Inizia la lezione successiva: 8.1 Ottenere i dati da Internet

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