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 layoutAdapter
,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
conRecyclerView
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:
- 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.
- 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.
- Apri SleepNightAdapter.kt.
- Sotto la classe
SleepNightListener
, nel livello superiore, definisci una classesealed
denominataDataItem
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 per un'altra parte del codice definire un nuovo tipo diDataItem
che potrebbe danneggiare l'adattatore.
sealed class DataItem {
}
- Nel corpo della classe
DataItem
, definisci due classi che rappresentano i diversi tipi di elementi di dati. Il primo è unSleepNightItem
, che è un wrapper attorno a unSleepNight
, quindi richiede un unico valore chiamatosleepNight
. Per farne parte nella classe chiusa, fai in modo che l'estensione venga estesa aDataItem
.
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 sarà presente una sola istanza diHeader
. Ribadisci che deve estendereDataItem
.
object Header: DataItem()
- All'interno di
DataItem
, a livello di classe, definisci una proprietàabstract
Long
denominataid
. Quando l'adattatore utilizzaDiffUtil
per determinare se e come un elemento è cambiato,DiffItemCallback
deve conoscere l'ID di ciascun elemento. Verrà visualizzato un errore, poichéSleepNightItem
eHeader
devono sostituire la 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 (ossia, -2 alla potenza di 63). Pertanto, non ci saranno conflitti con nessunnightId
esistente.
override val id = Long.MIN_VALUE
- 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
- 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" />
- Estrai
"Sleep Results"
in una risorsa stringa e chiamaloheader_text
.
<string name="header_text">Sleep Results</string>
- In SleepNightAdapter.kt, all'interno di
SleepNightAdapter
, sopra la classeViewHolder
, crea una nuova classeTextViewHolder
. Questa classe aumenta il layout textview.xml e restituisce un'istanzaTextViewHolder
. Poiché hai eseguito questa operazione in precedenza, ecco il codice che dovrai importare:View
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
. Invece di supportare un solo tipo di ViewHolder
, deve essere in grado di utilizzare qualsiasi tipo di titolare della vista.
Tipi di elementi
- A
SleepNightAdapter.kt
, al livello superiore, sotto le istruzioniimport
e sopraSleepNightAdapter
, 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
- All'interno di
SleepNightAdapter
, crea una funzione che sostituiscagetItemViewType()
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
- 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 essere simile a quella mostrata di seguito.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
Aggiornamento 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 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()
- Cambia il tipo di parametro di
onBindViewHolder()
daViewHolder
aRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Aggiungi una condizione per assegnare dati al titolare della vista solo se quest'ultimo è un
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Trasmetti il tipo di oggetto restituito da
getItem()
aDataItem.SleepNightItem
. La funzioneonBindViewHolder()
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
- Modifica i metodi in
SleepNightDiffCallback
per utilizzare la nuova classeDataItem
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
- All'interno di
SleepNightAdapter
, sottoonCreateViewHolder()
, definisci una funzioneaddHeaderAndSubmitList()
come mostrato di seguito. Questa funzione richiede un elenco diSleepNight
. Invece di utilizzaresubmitList()
, fornito daListAdapter
, per inviare l'elenco, dovrai utilizzare 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 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)
- Apri SleepTrackerFragment.kt e cambia la chiamata in
submitList()
inaddHeaderAndSubmitList()
.
- 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:
- Al livello superiore della classe
SleepNightAdapter
, definisci un elementoCoroutineScope
conDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- In
addHeaderAndSubmitList()
, avvia una coroutine nel dispositivoadapterScope
per manipolare l'elenco. Passa quindi al contestoDispatchers.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)
}
}
}
- 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.
- Apri SleepTrackerFragment.kt.
- Individua il codice verso cui definisci
manager
, verso la fine dionCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Sotto
manager
, definiscimanager.spanSizeLookup
, come mostrato. Devi preparare unobject
perchésetSpanSizeLookup
non prende una lambda. Per creare un elementoobject
in Kotlin, digitaobject : classname
, in questo casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Potresti ricevere un errore di compilazione per chiamare il costruttore. In questo caso, apri il menu dell'intent con
Option+Enter
(Mac) oAlt+Enter
(Windows) per applicare la chiamata costruttore.
- Dopodiché riceverai un errore su
object
che ti informa che devi sostituire i metodi. Posiziona il cursore suobject
, premiOption+Enter
(Mac) oAlt+Enter
(Windows) per aprire il menu delle intenzioni, quindi sostituisci il metodogetSpanSize()
.
- 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
}
}
- 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 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 corsoDataItem
. - Crea una funzione
addHeaderAndSubmitList()
che utilizzi le coroutine per aggiungere l'intestazione al set di dati, quindi chiamisubmitList()
. - 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:
Per i link ad altri codelab in questo corso, consulta la pagina di destinazione di Android Kotlin Fundamentals.