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 layoutAdapter
,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 unRecyclerView
per aggiungere elementi con un layout diverso. Nello specifico, come utilizzare un secondoViewHolder
per aggiungere un'intestazione sopra gli elementi visualizzati inRecyclerView
.
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.
- 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.
- Apri SleepNightAdapter.kt.
- Sotto la classe
SleepNightListener
, a livello superiore, definisci una classesealed
chiamataDataItem
che rappresenta un elemento di dati.
Una classesealed
definisce un tipo chiuso, il che significa che tutte le sottoclassi diDataItem
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 diDataItem
che potrebbe danneggiare l'adattatore.
sealed class DataItem {
}
- All'interno del corpo della classe
DataItem
, definisci due classi che rappresentano i diversi tipi di elementi di dati. Il primo è unSleepNightItem
, un wrapper intorno a unSleepNight
, quindi accetta un singolo valore denominatosleepNight
. Per includerlo nel corso sigillato, estendilo fino al giornoDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- La seconda classe è
Header
, per rappresentare un'intestazione. Poiché un'intestazione non contiene dati effettivi, puoi dichiararla comeobject
. Ciò significa che esisterà sempre e solo un'istanza diHeader
. Ancora una volta, fallo estendereDataItem
.
object Header: DataItem()
- All'interno di
DataItem
, a livello di corso, definisci una proprietàabstract
Long
denominataid
. Quando l'adattatore utilizzaDiffUtil
per determinare se e come un elemento è cambiato,DiffItemCallback
deve conoscere l'ID di ogni elemento. Visualizzerai un errore perchéSleepNightItem
eHeader
devono eseguire l'override della proprietà astrattaid
.
abstract val id: Long
- In
SleepNightItem
, sostituisciid
per restituirenightId
.
override val id = sleepNight.nightId
- In
Header
, esegui l'override diid
per restituireLong.MIN_VALUE
, che è un numero molto, molto piccolo (letteralmente, -2 elevato alla potenza di 63). Pertanto, non entrerà mai in conflitto con alcunnightId
esistente.
override val id = Long.MIN_VALUE
- 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
- 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" />
- Estrai
"Sleep Results"
in una risorsa stringa e chiamalaheader_text
.
<string name="header_text">Sleep Results</string>
- In SleepNightAdapter.kt, all'interno di
SleepNightAdapter
, sopra la classeViewHolder
, crea una nuova classeTextViewHolder
. Questa classe gonfia il layout textview.xml e restituisce un'istanzaTextViewHolder
. Poiché l'hai già fatto, ecco il codice e dovrai importareView
eR
:
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
- In
SleepNightAdapter.kt
, a livello superiore, sotto le istruzioniimport
e sopraSleepNightAdapter
, definisci due costanti per i tipi di visualizzazione.
IlRecyclerView
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
- All'interno di
SleepNightAdapter
, crea una funzione per eseguire l'override digetItemViewType()
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
- Nella definizione di
SleepNightAdapter
, aggiorna il primo argomento perListAdapter
daSleepNight
aDataItem
. - Nella definizione di
SleepNightAdapter
, modifica il secondo argomento generico perListAdapter
daSleepNightAdapter.ViewHolder
aRecyclerView.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()
- Modifica la firma di
onCreateViewHolder()
per restituire unRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- 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()
- Modifica il tipo di parametro di
onBindViewHolder()
daViewHolder
aRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Aggiungi una condizione per assegnare i dati al titolare della visualizzazione solo se è un
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Esegui il cast del tipo di oggetto restituito da
getItem()
aDataItem.SleepNightItem
. La funzioneonBindViewHolder()
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
- Modifica i metodi in
SleepNightDiffCallback
per utilizzare la nuova classeDataItem
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
- All'interno di
SleepNightAdapter
, sottoonCreateViewHolder()
, definisci una funzioneaddHeaderAndSubmitList()
come mostrato di seguito. Questa funzione accetta un elenco diSleepNight
. Anziché utilizzaresubmitList()
, fornito daListAdapter
, per inviare l'elenco, utilizzerai questa funzione per aggiungere un'intestazione e poi inviare l'elenco.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- 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)
- Apri SleepTrackerFragment.kt e modifica la chiamata a
submitList()
inaddHeaderAndSubmitList()
.
- 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:
- Al livello superiore all'interno della classe
SleepNightAdapter
, definisci unCoroutineScope
conDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- In
addHeaderAndSubmitList()
, avvia una coroutine inadapterScope
per manipolare l'elenco. Poi passa al contestoDispatchers.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)
}
}
}
- 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.
- Apri SleepTrackerFragment.kt.
- Trova il codice in cui definisci
manager
, verso la fine dionCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Sotto
manager
, definiscimanager.spanSizeLookup
, come mostrato. Devi creare unobject
perchésetSpanSizeLookup
non accetta un lambda. Per creare unobject
in Kotlin, digitaobject : classname
, in questo casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Potresti ricevere un errore del compilatore per chiamare il costruttore. In questo caso, apri il menu Intenzione con
Option+Enter
(Mac) oAlt+Enter
(Windows) per applicare la chiamata al costruttore.
- Poi riceverai un errore su
object
che ti dice che devi eseguire l'override dei metodi. Posiziona il cursore suobject
, premiOption+Enter
(Mac) oAlt+Enter
(Windows) per aprire il menu degli intent, quindi esegui l'override del metodogetSpanSize()
.
- 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
}
}
- 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"
- 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 classeDataItem
. - Crea una funzione
addHeaderAndSubmitList()
che utilizza le coroutine per aggiungere l'intestazione al set di dati e poi chiamasubmitList()
. - 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:
Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab di Android Kotlin Fundamentals.