Android Kotlin Fundamentals 08.2: Loading and displaying images from the internet

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

Nel codelab precedente hai imparato a recuperare i dati da un servizio web e ad analizzare la risposta in un oggetto dati. In questo codelab, amplierai queste conoscenze per caricare e visualizzare foto da un URL web. Inoltre, rivedrai come creare un RecyclerView e utilizzarlo per visualizzare una griglia di immagini nella pagina di panoramica.

Cosa devi già sapere

  • Come creare e utilizzare i frammenti.
  • Come utilizzare i componenti dell'architettura, inclusi view model, fabbriche di view model, trasformazioni e LiveData.
  • Come recuperare JSON da un servizio web REST e analizzare i dati in oggetti Kotlin utilizzando le librerie Retrofit e Moshi.
  • Come creare un layout a griglia con un RecyclerView.
  • Come funzionano Adapter, ViewHolder e DiffUtil.

Cosa imparerai a fare

  • Come utilizzare la libreria Glide per caricare e visualizzare un'immagine da un URL web.
  • Come utilizzare un RecyclerView e un adattatore a griglia per visualizzare una griglia di immagini.
  • Come gestire i potenziali errori durante il download e la visualizzazione delle immagini.

Attività previste

  • Modifica l'app MarsRealEstate per ottenere l'URL dell'immagine dai dati della proprietà Mars e utilizza Glide per caricare e visualizzare l'immagine.
  • Aggiungi un'animazione di caricamento e un'icona di errore all'app.
  • Utilizza un RecyclerView per visualizzare una griglia di immagini di proprietà di Marte.
  • Aggiungi la gestione dello stato e degli errori a RecyclerView.

In questo codelab (e in quelli correlati), lavorerai con un'app chiamata MarsRealEstate, che mostra le proprietà in vendita su Marte. L'app si connette a un server internet per recuperare e visualizzare i dati delle proprietà, inclusi dettagli come il prezzo e se la proprietà è disponibile per la vendita o l'affitto. Le immagini che rappresentano ogni proprietà sono foto reali di Marte scattate dai rover marziani della NASA.

La versione dell'app che crei in questo codelab riempie la pagina della panoramica, che mostra una griglia di immagini. Le immagini fanno parte dei dati della proprietà che la tua app riceve dal servizio web immobiliare Mars. La tua app utilizzerà la libreria Glide per caricare e visualizzare le immagini e un RecyclerView per creare il layout a griglia per le immagini. La tua app gestirà anche gli errori di rete in modo controllato.

Visualizzare una foto da un URL web può sembrare semplice, ma è necessario un lavoro di progettazione piuttosto complesso per farla funzionare correttamente. L'immagine deve essere scaricata, memorizzata nel buffer e decodificata dal formato compresso a un formato utilizzabile da Android. L'immagine deve essere memorizzata nella cache in una cache in memoria, in una cache basata sull'archiviazione o in entrambe. Tutto questo deve avvenire in thread in background a bassa priorità, in modo che l'interfaccia utente rimanga reattiva. Inoltre, per ottenere le migliori prestazioni di rete e della CPU, ti consigliamo di recuperare e decodificare più immagini contemporaneamente. Imparare a caricare in modo efficace le immagini dalla rete potrebbe essere un codelab a sé stante.

Fortunatamente, puoi utilizzare una libreria sviluppata dalla community chiamata Glide per scaricare, memorizzare nel buffer, decodificare e memorizzare nella cache le tue immagini. Glide ti fa risparmiare molto lavoro rispetto a se dovessi fare tutto da zero.

Glide ha bisogno di due cose:

  • L'URL dell'immagine che vuoi caricare e mostrare.
  • Un oggetto ImageView per visualizzare l'immagine.

In questa attività, imparerai a utilizzare Glide per visualizzare una singola immagine dal servizio web immobiliare. Visualizzi l'immagine che rappresenta la prima proprietà di Marte nell'elenco delle proprietà restituite dal servizio web. Ecco gli screenshot prima e dopo:

Passaggio 1: aggiungi la dipendenza Glide

  1. Apri l'app MarsRealEstate dell'ultimo codelab. Se non hai l'app, puoi scaricare MarsRealEstateNetwork qui.
  2. Esegui l'app per vedere cosa fa. (Mostra i dettagli di testo di una proprietà ipoteticamente disponibile su Marte.)
  3. Apri build.gradle (Module: app).
  4. Nella sezione dependencies, aggiungi questa riga per la libreria Glide:
implementation "com.github.bumptech.glide:glide:$version_glide"


Tieni presente che il numero di versione è già definito separatamente nel file Gradle del progetto.

  1. Fai clic su Sincronizza ora per ricompilare il progetto con la nuova dipendenza.

Passaggio 2: aggiorna il modello di visualizzazione

Poi aggiorna la classe OverviewViewModel per includere i dati in tempo reale per una singola proprietà di Marte.

  1. Apri overview/OverviewViewModel.kt. Appena sotto LiveData per _response, aggiungi dati live interni (modificabili) ed esterni (immutabili) per un singolo oggetto MarsProperty.

    Importa il corso MarsProperty (com.example.android.marsrealestate.network.MarsProperty) quando richiesto.
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. Nel metodo getMarsRealEstateProperties(), trova la riga all'interno del blocco try/catch {} che imposta _response.value sul numero di proprietà. Aggiungi il test mostrato di seguito. Se sono disponibili oggetti MarsProperty, questo test imposta il valore di _property LiveData sulla prima proprietà in listResult.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

Il blocco try/catch {} completo ora ha il seguente aspetto:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. Apri il file res/layout/fragment_overview.xml. Nell'elemento <TextView>, modifica android:text in modo che venga associato al componente imgSrcUrl di property LiveData:
android:text="@{viewModel.property.imgSrcUrl}"
  1. Esegui l'app. TextView mostra solo l'URL dell'immagine nella prima proprietà di Marte. Finora hai configurato solo il modello di visualizzazione e i dati in tempo reale per l'URL.

Passaggio 3: crea un adattatore di binding e chiama Glide

Ora hai l'URL di un'immagine da visualizzare ed è il momento di iniziare a lavorare con Glide per caricarla. In questo passaggio, utilizzi un adattatore di binding per estrarre l'URL da un attributo XML associato a un ImageView e utilizzi Glide per caricare l'immagine. Gli adattatori di binding sono metodi di estensione che si trovano tra una visualizzazione e i dati associati per fornire un comportamento personalizzato quando i dati cambiano. In questo caso, il comportamento personalizzato consiste nel chiamare Glide per caricare un'immagine da un URL in un ImageView.

  1. Apri BindingAdapters.kt. Questo file conterrà gli adattatori di binding che utilizzi in tutta l'app.
  2. Crea una funzione bindImage() che accetti un ImageView e un String come parametri. Annota la funzione con @BindingAdapter. L'annotazione @BindingAdapter indica all'associazione di dati che vuoi che questo adattatore di associazione venga eseguito quando un elemento XML ha l'attributo imageUrl.

    Importa androidx.databinding.BindingAdapter e android.widget.ImageView quando richiesto.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. All'interno della funzione bindImage(), aggiungi un blocco let {} per l'argomento imgUrl:
imgUrl?.let { 
}
  1. All'interno del blocco let {}, aggiungi la riga mostrata di seguito per convertire la stringa URL (dal file XML) in un oggetto Uri. Importa androidx.core.net.toUri quando richiesto.

    Vuoi che l'oggetto Uri finale utilizzi lo schema HTTPS, perché il server da cui estrai le immagini lo richiede. Per utilizzare lo schema HTTPS, aggiungi buildUpon.scheme("https") al generatore toUri. Il metodo toUri() è una funzione di estensione Kotlin della libreria principale Android KTX, quindi sembra solo far parte della classe String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. All'interno di let {}, chiama Glide.with() per caricare l'immagine dall'oggetto Uri in ImageView. Importa com.bumptech.glide.Glide quando richiesto.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

Passaggio 4: aggiorna il layout e i frammenti

Anche se Glide ha caricato l'immagine, non c'è ancora nulla da vedere. Il passaggio successivo consiste nell'aggiornamento del layout e dei fragment con un ImageView per visualizzare l'immagine.

  1. Apri res/layout/gridview_item.xml. Questo è il file di risorse di layout che utilizzerai per ogni elemento di RecyclerView più avanti nel codelab. Lo utilizzi temporaneamente qui per mostrare solo la singola immagine.
  2. Sopra l'elemento <ImageView>, aggiungi un elemento <data> per l'associazione di dati e associa la classe OverviewViewModel:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. Aggiungi un attributo app:imageUrl all'elemento ImageView per utilizzare il nuovo adattatore di binding per il caricamento delle immagini:
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. Apri overview/OverviewFragment.kt. Nel metodo onCreateView(), commenta la riga che gonfia la classe FragmentOverviewBinding e la assegna alla variabile di binding. Questa modifica è temporanea e potrai tornare a questa impostazione in un secondo momento.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Aggiungi una riga per gonfiare la classe GridViewItemBinding. Importa com.example.android.marsrealestate. databinding.GridViewItemBinding quando richiesto.
val binding = GridViewItemBinding.inflate(inflater)
  1. Esegui l'app. Ora dovresti vedere la foto dell'immagine del primo MarsProperty nell'elenco dei risultati.

Passaggio 5: aggiungi immagini di caricamento ed errore semplici

Glide può migliorare l'esperienza utente mostrando un'immagine segnaposto durante il caricamento dell'immagine e un'immagine di errore se il caricamento non va a buon fine, ad esempio se l'immagine è mancante o danneggiata. In questo passaggio, aggiungi questa funzionalità all'adattatore di binding e al layout.

  1. Apri res/drawable/ic_broken_image.xml e fai clic sulla scheda Anteprima a destra. Per l'immagine di errore, utilizzi l'icona di immagine non disponibile che è disponibile nella libreria di icone integrata. Questa risorsa drawable vettoriale utilizza l'attributo android:tint per colorare l'icona di grigio.

  1. Apri res/drawable/loading_animation.xml. Questa risorsa disegnabile è un'animazione definita con il tag <animate-rotate>. L'animazione ruota un elemento disegnabile dell'immagine, loading_img.xml, attorno al punto centrale. Non vedi l'animazione nell'anteprima.

  1. Torna al file BindingAdapters.kt. Nel metodo bindImage(), aggiorna la chiamata a Glide.with() per chiamare la funzione apply() tra load() e into(). Importa com.bumptech.glide.request.RequestOptions quando richiesto.

    Questo codice imposta l'immagine di caricamento del segnaposto da utilizzare durante il caricamento (la risorsa disegnabile loading_animation). Il codice imposta anche un'immagine da utilizzare se il caricamento dell'immagine non va a buon fine (la risorsa disegnabile broken_image). Il metodo bindImage() completo ora è il seguente:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = 
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  1. Esegui l'app. A seconda della velocità della connessione di rete, potresti vedere brevemente l'immagine di caricamento mentre Glide scarica e visualizza l'immagine della proprietà. Tuttavia, non vedrai ancora l'icona dell'immagine non funzionante, anche se disattivi la rete. Risolverai il problema nell'ultima parte del codelab.

Ora l'app carica le informazioni sulla proprietà da internet. Utilizzando i dati del primo elemento dell'elenco MarsProperty, hai creato una proprietà LiveData nel modello di visualizzazione e hai utilizzato l'URL dell'immagine dei dati della proprietà per compilare un ImageView. Tuttavia, l'obiettivo è che la tua app mostri una griglia di immagini, quindi devi utilizzare un RecyclerView con un GridLayoutManager.

Passaggio 1: aggiorna il modello di visualizzazione

Al momento, il modello di visualizzazione ha un _property LiveData che contiene un oggetto MarsProperty, il primo nell'elenco delle risposte del servizio web. In questo passaggio, modifichi LiveData in modo che contenga l'intero elenco di oggetti MarsProperty.

  1. Apri overview/OverviewViewModel.kt.
  2. Modifica la variabile privata _property in _properties. Modifica il tipo in un elenco di oggetti MarsProperty.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. Sostituisci i dati in tempo reale esterni property con properties. Aggiungi l'elenco anche al tipo LiveData qui:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. Scorri verso il basso fino al metodo getMarsRealEstateProperties(). All'interno del blocco try {}, sostituisci l'intero test che hai aggiunto nell'attività precedente con la riga mostrata di seguito. Poiché la variabile listResult contiene un elenco di oggetti MarsProperty, puoi assegnarla a _properties.value anziché verificare se la risposta è andata a buon fine.
_properties.value = listResult

L'intero blocco try/catch ora ha il seguente aspetto:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

Passaggio 2: aggiorna i layout e i fragment

Il passaggio successivo consiste nel modificare il layout e i frammenti dell'app per utilizzare una visualizzazione elenco e un layout a griglia, anziché la visualizzazione di una singola immagine.

  1. Apri res/layout/gridview_item.xml. Modifica il binding dei dati da OverviewViewModel a MarsProperty e rinomina la variabile in "property".
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. In <ImageView>, modifica l'attributo app:imageUrl in modo che faccia riferimento all'URL dell'immagine nell'oggetto MarsProperty:
app:imageUrl="@{property.imgSrcUrl}"
  1. Apri overview/OverviewFragment.kt. In onCreateview(), rimuovi il commento dalla riga che gonfia FragmentOverviewBinding. Elimina o commenta la riga che gonfia GridViewBinding. Queste modifiche annullano le modifiche temporanee apportate nell'ultima attività.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. Apri res/layout/fragment_overview.xml. Elimina l'intero elemento <TextView>.
  2. Aggiungi invece questo elemento <RecyclerView>, che utilizza un layout GridLayoutManager e grid_view_item per un singolo elemento:
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            app:layoutManager=
               "androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:spanCount="2"
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />

Passaggio 3: aggiungi l'adattatore per griglia di foto

Ora il layout fragment_overview ha un RecyclerView, mentre il layout grid_view_item ha un solo ImageView. In questo passaggio, colleghi i dati a RecyclerView tramite un adattatore RecyclerView.

  1. Apri overview/PhotoGridAdapter.kt.
  2. Crea la classe PhotoGridAdapter con i parametri del costruttore mostrati di seguito. La classe PhotoGridAdapter estende ListAdapter, il cui costruttore richiede il tipo di elemento dell'elenco, il view holder e un'implementazione DiffUtil.ItemCallback.

    Importa i corsi androidx.recyclerview.widget.ListAdapter e com.example.android.marsrealestate.network.MarsProperty quando richiesto. Nei passaggi successivi, implementa le altre parti mancanti di questo costruttore che generano errori.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. Fai clic in un punto qualsiasi della classe PhotoGridAdapter e premi Control+i per implementare i metodi ListAdapter, ovvero onCreateViewHolder() e onBindViewHolder().
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. Alla fine della definizione della classe PhotoGridAdapter, dopo i metodi appena aggiunti, aggiungi una definizione di oggetto companion per DiffCallback, come mostrato di seguito.

    Importa androidx.recyclerview.widget.DiffUtil quando richiesto.

    L'oggetto DiffCallback estende DiffUtil.ItemCallback con il tipo di oggetto che vuoi confrontare: MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Premi Control+i per implementare i metodi di confronto per questo oggetto, ovvero areItemsTheSame() e areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. Per il metodo areItemsTheSame(), rimuovi TODO. Utilizza l'operatore di uguaglianza referenziale di Kotlin (===), che restituisce true se i riferimenti agli oggetti per oldItem e newItem sono gli stessi.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. Per areContentsTheSame(), utilizza l'operatore di uguaglianza standard solo sull'ID di oldItem e newItem.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. Sempre all'interno della classe PhotoGridAdapter, sotto l'oggetto complementare, aggiungi una definizione di classe interna per MarsPropertyViewHolder, che estende RecyclerView.ViewHolder.

    Importa androidx.recyclerview.widget.RecyclerView e com.example.android.marsrealestate.databinding.GridViewItemBinding quando richiesto.

    Hai bisogno della variabile GridViewItemBinding per associare MarsProperty al layout, quindi passala a MarsPropertyViewHolder. Poiché la classe base ViewHolder richiede una vista nel suo costruttore, le passi la vista radice del binding.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. In MarsPropertyViewHolder, crea un metodo bind() che accetta un oggetto MarsProperty come argomento e imposta binding.property su quell'oggetto. Chiama executePendingBindings() dopo aver impostato la proprietà, in modo che l'aggiornamento venga eseguito immediatamente.
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. In onCreateViewHolder(), rimuovi TODO e aggiungi la riga mostrata di seguito. Importa android.view.LayoutInflater quando richiesto.

    Il metodo onCreateViewHolder() deve restituire un nuovo MarsPropertyViewHolder, creato gonfiando GridViewItemBinding e utilizzando LayoutInflater dal contesto ViewGroup principale.
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. Nel metodo onBindViewHolder(), rimuovi TODO e aggiungi le righe mostrate di seguito. Qui chiami getItem() per ottenere l'oggetto MarsProperty associato alla posizione RecyclerView corrente e poi passi questa proprietà al metodo bind() in MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

Passaggio 4: aggiungi l'adattatore di rilegatura e collega le parti

Infine, utilizza un BindingAdapter per inizializzare PhotoGridAdapter con l'elenco di oggetti MarsProperty. L'utilizzo di un BindingAdapter per impostare i dati RecyclerView fa sì che il binding dei dati osservi automaticamente LiveData per l'elenco degli oggetti MarsProperty. L'adattatore di binding viene chiamato automaticamente quando l'elenco MarsProperty cambia.

  1. Apri BindingAdapters.kt.
  2. Alla fine del file, aggiungi un metodo bindRecyclerView() che accetta un RecyclerView e un elenco di oggetti MarsProperty come argomenti. Aggiungi un'annotazione a questo metodo con @BindingAdapter.

    Importa androidx.recyclerview.widget.RecyclerView e com.example.android.marsrealestate.network.MarsProperty quando richiesto.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. All'interno della funzione bindRecyclerView(), esegui il cast di recyclerView.adapter in PhotoGridAdapter e chiama adapter.submitList() con i dati. Indica a RecyclerView quando è disponibile un nuovo elenco.

Importa com.example.android.marsrealestate.overview.PhotoGridAdapter quando richiesto.

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. Apri res/layout/fragment_overview.xml. Aggiungi l'attributo app:listData all'elemento RecyclerView e impostalo su viewmodel.properties utilizzando l'associazione dei dati.
app:listData="@{viewModel.properties}"
  1. Apri overview/OverviewFragment.kt. In onCreateView(), appena prima della chiamata a setHasOptionsMenu(), inizializza l'adattatore RecyclerView in binding.photosGrid a un nuovo oggetto PhotoGridAdapter.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. Esegui l'app. Dovresti vedere una griglia di immagini di MarsProperty. Mentre scorri per visualizzare le nuove immagini, l'app mostra l'icona di avanzamento del caricamento prima di visualizzare l'immagine stessa. Se attivi la modalità aereo, le immagini che non sono ancora state caricate vengono visualizzate come icone di immagini non funzionanti.

L'app MarsRealEstate mostra l'icona di immagine non disponibile quando non è possibile recuperare un'immagine. Tuttavia, in assenza di rete, l'app mostra una schermata vuota.

Questa non è un'esperienza utente ottimale. In questa attività, aggiungi la gestione di base degli errori per dare all'utente un'idea migliore di ciò che sta succedendo. Se internet non è disponibile, l'app mostrerà l'icona di errore di connessione. Mentre l'app recupera l'elenco MarsProperty, viene visualizzata l'animazione di caricamento.

Passaggio 1: aggiungi lo stato al modello di visualizzazione

Per iniziare, crea un LiveData nel modello di visualizzazione per rappresentare lo stato della richiesta web. Esistono tre stati da considerare: caricamento, riuscito e non riuscito. Lo stato di caricamento si verifica mentre attendi i dati nella chiamata a await().

  1. Apri overview/OverviewViewModel.kt. Nella parte superiore del file (dopo le importazioni, prima della definizione della classe), aggiungi un enum per rappresentare tutti gli stati disponibili:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. Rinomina le definizioni dei dati live interni ed esterni _response in tutta la classe OverviewViewModel in _status. Poiché in precedenza in questo codelab hai aggiunto il supporto per _properties LiveData, la risposta completa del servizio web non è stata utilizzata. Qui hai bisogno di un LiveData per tenere traccia dello stato attuale, quindi puoi semplicemente rinominare le variabili esistenti.

Inoltre, modifica i tipi da String a MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. Scorri verso il basso fino al metodo getMarsRealEstateProperties() e aggiorna _response a _status anche qui. Modifica la stringa "Success" in modo che corrisponda allo stato MarsApiStatus.DONE e la stringa "Failure" in modo che corrisponda a MarsApiStatus.ERROR.
  2. Aggiungi uno stato MarsApiStatus.LOADING all'inizio del blocco try {}, prima della chiamata a await(). Questo è lo stato iniziale mentre la coroutine è in esecuzione e stai aspettando i dati. Il blocco try/catch {} completo ora ha il seguente aspetto:
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. Dopo lo stato di errore nel blocco catch {}, imposta _properties LiveData su un elenco vuoto. In questo modo, RecyclerView.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

Passaggio 2: aggiungi un adattatore di binding per ImageView di stato

Ora hai uno stato nel modello di visualizzazione, ma si tratta solo di un insieme di stati. Come si fa a farlo apparire nell'app stessa? In questo passaggio, utilizzi un ImageView, collegato al data binding, per visualizzare le icone per gli stati di caricamento ed errore. Quando l'app è in stato di caricamento o di errore, ImageView dovrebbe essere visibile. Al termine del caricamento dell'app, ImageView dovrebbe essere invisibile.

  1. Apri BindingAdapters.kt. Aggiungi un nuovo adattatore di binding chiamato bindStatus() che accetta un valore ImageView e un valore MarsApiStatus come argomenti. Importa com.example.android.marsrealestate.overview.MarsApiStatus quando richiesto.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. Aggiungi un when {} all'interno del metodo bindStatus() per passare da uno stato all'altro.
when (status) {

}
  1. All'interno di when {}, aggiungi un caso per lo stato di caricamento (MarsApiStatus.LOADING). Per questo stato, imposta ImageView su visibile e assegna l'animazione di caricamento. Si tratta dello stesso drawable di animazione che hai utilizzato per Glide nell'attività precedente. Importa android.view.View quando richiesto.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. Aggiungi un caso per lo stato di errore, ovvero MarsApiStatus.ERROR. Analogamente a quanto fatto per lo stato LOADING, imposta lo stato ImageView su visibile e riutilizza la risorsa disegnabile di errore di connessione.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. Aggiungi un caso per lo stato completato, ovvero MarsApiStatus.DONE. In questo caso la risposta ha esito positivo, quindi disattiva la visibilità dello stato ImageView per nasconderlo.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Passaggio 3: aggiungi ImageView di stato al layout

  1. Apri res/layout/fragment_overview.xml. Sotto l'elemento RecyclerView, all'interno di ConstraintLayout, aggiungi ImageView mostrato di seguito.

    Questo ImageView ha gli stessi vincoli di RecyclerView. Tuttavia, la larghezza e l'altezza utilizzano wrap_content per centrare l'immagine anziché allungarla per riempire la visualizzazione. Nota anche l'attributo app:marsApiStatus, che chiama la visualizzazione BindingAdapter quando la proprietà di stato nel modello di visualizzazione cambia.
<ImageView
   android:id="@+id/status_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:marsApiStatus="@{viewModel.status}" />
  1. Attiva la modalità aereo nell'emulatore o sul dispositivo per simulare una connessione di rete mancante. Compila ed esegui l'app e nota che viene visualizzata l'immagine di errore:

  1. Tocca il pulsante Indietro per chiudere l'app e disattivare la modalità aereo. Utilizza la schermata delle app recenti per tornare all'app. A seconda della velocità della connessione di rete, potresti vedere un indicatore di caricamento molto breve quando l'app esegue una query sul servizio web prima che le immagini inizino a caricarsi.

Progetto Android Studio: MarsRealEstateGrid

  • Per semplificare il processo di gestione delle immagini, utilizza la libreria Glide per scaricare, memorizzare nel buffer, decodificare e memorizzare nella cache le immagini nella tua app.
  • Per caricare un'immagine da internet, Glide ha bisogno di due cose: l'URL di un'immagine e un oggetto ImageView in cui inserirla. Per specificare queste opzioni, utilizza i metodi load() e into() con Glide.
  • Gli adattatori di binding sono metodi di estensione che si trovano tra una visualizzazione e i dati associati. Gli adattatori di binding forniscono un comportamento personalizzato quando i dati cambiano, ad esempio per chiamare Glide per caricare un'immagine da un URL in un ImageView.
  • Gli adattatori di binding sono metodi di estensione annotati con l'annotazione @BindingAdapter.
  • Per aggiungere opzioni alla richiesta Glide, utilizza il metodo apply(). Ad esempio, utilizza apply() con placeholder() per specificare una risorsa disegnabile di caricamento e apply() con error() per specificare una risorsa disegnabile di errore.
  • Per produrre una griglia di immagini, utilizza un RecyclerView con un GridLayoutManager.
  • Per aggiornare l'elenco delle proprietà quando cambia, utilizza un adattatore di binding tra RecyclerView e il layout.

Corso Udacity:

Documentazione per sviluppatori Android:

Altro:

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 metodo Glide utilizzi per indicare il ImageView che conterrà l'immagine caricata?

into()

with()

imageview()

apply()

Domanda 2

Come si specifica un'immagine segnaposto da mostrare durante il caricamento di Glide?

▢ Utilizza il metodo into() con una risorsa disegnabile.

▢ Utilizza RequestOptions() e chiama il metodo placeholder() con una risorsa disegnabile.

▢ Assegna la proprietà Glide.placeholder a una risorsa disegnabile.

▢ Utilizza RequestOptions() e chiama il metodo loadingImage() con una risorsa disegnabile.

Domanda 3

Come si indica che un metodo è un adattatore di binding?

▢ Chiama il metodo setBindingAdapter() su LiveData.

▢ Inserisci il metodo in un file Kotlin denominato BindingAdapters.kt.

▢ Utilizza l'attributo android:adapter nel layout XML.

▢ Annota il metodo con @BindingAdapter.

Inizia la lezione successiva: 8.3 Filtri e visualizzazioni dettagliate con dati internet

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