Android Kotlin Fundamentals 08.2: Bilder aus dem Internet laden und anzeigen

Dieses Codelab ist Teil des Kurses „Grundlagen von Android und Kotlin“. Sie können diesen Kurs am besten nutzen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu den Grundlagen von Android und Kotlin aufgeführt.

Einführung

Im vorherigen Codelab haben Sie gelernt, wie Sie Daten von einem Webdienst abrufen und die Antwort in ein Datenobjekt parsen. In diesem Codelab bauen Sie auf diesem Wissen auf, um Fotos über eine Web-URL zu laden und anzuzeigen. Außerdem wird noch einmal gezeigt, wie Sie ein RecyclerView erstellen und damit ein Raster mit Bildern auf der Übersichtsseite anzeigen.

Was Sie bereits wissen sollten

  • Fragmente erstellen und verwenden
  • Verwendung von Architekturkomponenten wie Viewmodels, Viewmodelfactories, Transformationen und LiveData.
  • So rufen Sie JSON von einem REST-Webdienst ab und parsen die Daten mithilfe der Bibliotheken Retrofit und Moshi in Kotlin-Objekte.
  • So erstellen Sie ein Rasterlayout mit einem RecyclerView.
  • So funktionieren Adapter, ViewHolder und DiffUtil.

Lerninhalte

  • Verwendung der Glide-Bibliothek zum Laden und Anzeigen eines Bildes aus einer Web-URL.
  • So verwenden Sie einen RecyclerView und einen Rasteradapter, um ein Raster mit Bildern anzuzeigen.
  • So behandeln Sie potenzielle Fehler beim Herunterladen und Anzeigen der Bilder.

Aufgabe

  • Ändern Sie die MarsRealEstate-App so, dass die Bild-URL aus den Mars-Property-Daten abgerufen und das Bild mit Glide geladen und angezeigt wird.
  • Fügen Sie der App eine Ladeanimation und ein Fehlersymbol hinzu.
  • Verwenden Sie ein RecyclerView, um ein Raster mit Bildern von Mars-Grundstücken anzuzeigen.
  • Fügen Sie dem RecyclerView Status- und Fehlerbehandlung hinzu.

In diesem Codelab (und den zugehörigen Codelabs) arbeiten Sie mit einer App namens MarsRealEstate, in der zum Verkauf stehende Immobilien auf dem Mars angezeigt werden. Die App stellt eine Verbindung zu einem Internetserver her, um Immobiliendaten abzurufen und anzuzeigen, einschließlich Details wie Preis und ob die Immobilie zum Verkauf oder zur Vermietung verfügbar ist. Die Bilder, die die einzelnen Grundstücke darstellen, sind echte Fotos vom Mars, die von den Mars-Rovern der NASA aufgenommen wurden.

Die Version der App, die Sie in diesem Codelab erstellen, füllt die Übersichtsseite mit einem Raster aus Bildern. Die Bilder sind Teil der Property-Daten, die Ihre App vom Mars-Webdienst für Immobilien erhält. Ihre App verwendet die Glide-Bibliothek zum Laden und Anzeigen der Bilder und ein RecyclerView zum Erstellen des Rasterlayouts für die Bilder. Ihre App sollte auch Netzwerkfehler problemlos verarbeiten können.

Ein Foto über eine Web-URL anzuzeigen, klingt vielleicht einfach, aber es ist einiges an technischer Entwicklung erforderlich, damit es gut funktioniert. Das Bild muss heruntergeladen, gepuffert und aus seinem komprimierten Format in ein Bild decodiert werden, das von Android verwendet werden kann. Das Bild sollte in einem In-Memory-Cache, einem speicherbasierten Cache oder in beiden gespeichert werden. All dies muss in Hintergrundthreads mit niedriger Priorität erfolgen, damit die Benutzeroberfläche reaktionsfähig bleibt. Für eine optimale Netzwerk- und CPU-Leistung sollten Sie außerdem mehr als ein Bild gleichzeitig abrufen und decodieren. Das effektive Laden von Bildern aus dem Netzwerk könnte ein Codelab für sich sein.

Glücklicherweise können Sie die von der Community entwickelte Bibliothek Glide verwenden, um Ihre Bilder herunterzuladen, zu puffern, zu decodieren und im Cache zu speichern. Mit Glide haben Sie viel weniger Arbeit, als wenn Sie alles von Grund auf neu erstellen müssten.

Für Glide sind im Grunde zwei Dinge erforderlich:

  • Die URL des Bildes, das geladen und angezeigt werden soll.
  • Ein ImageView-Objekt zum Anzeigen des Bildes.

In dieser Aufgabe erfahren Sie, wie Sie mit Glide ein einzelnes Bild aus dem Immobilien-Webdienst anzeigen. Sie zeigen das Bild an, das die erste Mars-Property in der Liste der Properties darstellt, die vom Webdienst zurückgegeben werden. Hier sehen Sie die Screenshots vor und nach der Änderung:

Schritt 1: Glide-Abhängigkeit hinzufügen

  1. Öffnen Sie die MarsRealEstate-App aus dem letzten Codelab. Falls Sie die App noch nicht haben, können Sie MarsRealEstateNetwork hier herunterladen.
  2. Führen Sie die App aus, um zu sehen, was sie tut. (Es werden Textdetails zu einem Grundstück angezeigt, das hypothetisch auf dem Mars verfügbar ist.)
  3. Öffnen Sie build.gradle (Module: app).
  4. Fügen Sie im Abschnitt dependencies diese Zeile für die Glide-Bibliothek hinzu:
implementation "com.github.bumptech.glide:glide:$version_glide"


Die Versionsnummer ist bereits separat in der Gradle-Datei des Projekts definiert.

  1. Klicken Sie auf Jetzt synchronisieren, um das Projekt mit der neuen Abhängigkeit neu zu erstellen.

Schritt 2: View-Modell aktualisieren

Als Nächstes aktualisieren Sie die Klasse OverviewViewModel, damit sie Livedaten für eine einzelne Mars-Property enthält.

  1. Öffnen Sie overview/OverviewViewModel.kt. Fügen Sie direkt unter dem LiveData für das _response sowohl interne (veränderliche) als auch externe (unveränderliche) Live-Daten für ein einzelnes MarsProperty-Objekt hinzu.

    Importieren Sie die Klasse MarsProperty (com.example.android.marsrealestate.network.MarsProperty), wenn Sie dazu aufgefordert werden.
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. Suchen Sie in der Methode getMarsRealEstateProperties() die Zeile im try/catch {}-Block, in der _response.value auf die Anzahl der Attribute festgelegt wird. Fügen Sie den unten gezeigten Test hinzu. Wenn MarsProperty-Objekte verfügbar sind, wird mit diesem Test der Wert des _property LiveData auf die erste Eigenschaft in der listResult festgelegt.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

Der vollständige try/catch {}-Block sieht jetzt so aus:

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. Öffnen Sie die Datei res/layout/fragment_overview.xml. Ändern Sie im <TextView>-Element android:text, um die Bindung an die imgSrcUrl-Komponente des property LiveData herzustellen:
android:text="@{viewModel.property.imgSrcUrl}"
  1. Führen Sie die App aus. In TextView wird nur die URL des Bildes in der ersten Mars-Property angezeigt. Bisher haben Sie nur das View-Modell und die Live-Daten für diese URL eingerichtet.

Schritt 3: Binding-Adapter erstellen und Glide aufrufen

Sie haben jetzt die URL eines Bildes, das angezeigt werden soll. Nun können Sie mit Glide beginnen, dieses Bild zu laden. In diesem Schritt verwenden Sie einen Binding-Adapter, um die URL aus einem XML-Attribut abzurufen, das mit einem ImageView verknüpft ist. Anschließend verwenden Sie Glide, um das Bild zu laden. Binding-Adapter sind Erweiterungsmethoden, die zwischen einer Ansicht und gebundenen Daten liegen und benutzerdefiniertes Verhalten ermöglichen, wenn sich die Daten ändern. In diesem Fall wird Glide aufgerufen, um ein Bild aus einer URL in ein ImageView zu laden.

  1. Öffnen Sie BindingAdapters.kt. Diese Datei enthält die Bindungsadapter, die Sie in der gesamten App verwenden.
  2. Erstellen Sie eine bindImage()-Funktion, die ein ImageView und ein String als Parameter verwendet. Kommentieren Sie die Funktion mit @BindingAdapter. Die Annotation @BindingAdapter weist die Datenbindung an, diesen Bindungsadapter auszuführen, wenn ein XML-Element das Attribut imageUrl hat.

    Importieren Sie androidx.databinding.BindingAdapter und android.widget.ImageView, wenn Sie dazu aufgefordert werden.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. Fügen Sie in der Funktion bindImage() einen let {}-Block für das Argument imgUrl ein:
imgUrl?.let { 
}
  1. Fügen Sie im let {}-Block die unten gezeigte Zeile hinzu, um den URL-String (aus dem XML) in ein Uri-Objekt zu konvertieren. Importieren Sie androidx.core.net.toUri, wenn Sie dazu aufgefordert werden.

    Das endgültige Uri-Objekt soll das HTTPS-Schema verwenden, da der Server, von dem Sie die Bilder abrufen, dieses Schema erfordert. Wenn Sie das HTTPS-Schema verwenden möchten, hängen Sie buildUpon.scheme("https") an den toUri-Builder an. Die Methode toUri() ist eine Kotlin-Erweiterungsfunktion aus der Android KTX-Kernbibliothek. Sie sieht also so aus, als wäre sie Teil der Klasse String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. Rufen Sie in let {} Glide.with() auf, um das Bild aus dem Uri-Objekt in das ImageView zu laden. Importieren Sie com.bumptech.glide.Glide, wenn Sie dazu aufgefordert werden.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

Schritt 4: Layout und Fragmente aktualisieren

Das Bild wurde zwar von Glide geladen, aber es ist noch nichts zu sehen. Im nächsten Schritt aktualisieren Sie das Layout und die Fragmente mit einem ImageView, um das Bild anzuzeigen.

  1. Öffnen Sie res/layout/gridview_item.xml. Dies ist die Layoutressourcendatei, die Sie später im Codelab für jedes Element in der RecyclerView verwenden. Sie verwenden es hier vorübergehend, um nur das einzelne Bild anzuzeigen.
  2. Fügen Sie über dem <ImageView>-Element ein <data>-Element für die Datenbindung hinzu und binden Sie es an die OverviewViewModel-Klasse:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. Fügen Sie dem ImageView-Element ein app:imageUrl-Attribut hinzu, um den neuen Binding-Adapter für das Laden von Bildern zu verwenden:
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. Öffnen Sie overview/OverviewFragment.kt. Kommentieren Sie in der Methode onCreateView() die Zeile aus, mit der die Klasse FragmentOverviewBinding instanziiert und der Bindungsvariable zugewiesen wird. Das ist nur vorübergehend. Sie kehren später wieder dorthin zurück.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Fügen Sie stattdessen eine Zeile hinzu, um die Klasse GridViewItemBinding zu erweitern. Importieren Sie com.example.android.marsrealestate. databinding.GridViewItemBinding, wenn Sie dazu aufgefordert werden.
val binding = GridViewItemBinding.inflate(inflater)
  1. Führen Sie die App aus. In der Ergebnisliste sollte jetzt ein Foto des Bildes aus dem ersten MarsProperty angezeigt werden.

Schritt 5: Einfache Lade- und Fehlerbilder hinzufügen

Glide kann die Nutzerfreundlichkeit verbessern, indem während des Ladens des Bildes ein Platzhalterbild und bei einem Fehler beim Laden ein Fehlerbild angezeigt wird, z. B. wenn das Bild fehlt oder beschädigt ist. In diesem Schritt fügen Sie diese Funktion dem Binding-Adapter und dem Layout hinzu.

  1. Öffnen Sie res/drawable/ic_broken_image.xml und klicken Sie rechts auf den Tab Vorschau. Für das Fehlerbild verwenden Sie das Symbol für ein defektes Bild, das in der integrierten Symbolbibliothek verfügbar ist. In diesem Vektor-Drawable wird das Attribut android:tint verwendet, um das Symbol grau zu färben.

  1. Öffnen Sie res/drawable/loading_animation.xml. Dieses Drawable ist eine Animation, die mit dem Tag <animate-rotate> definiert wird. Bei der Animation wird ein Bild-Drawable, loading_img.xml, um den Mittelpunkt gedreht. Die Animation wird in der Vorschau nicht angezeigt.

  1. Kehren Sie zur Datei BindingAdapters.kt zurück. Aktualisieren Sie in der Methode bindImage() den Aufruf von Glide.with() so, dass die Funktion apply() zwischen load() und into() aufgerufen wird. com.bumptech.glide.request.RequestOptionsImportieren, wenn angefordert.

    Mit diesem Code wird das Platzhalter-Ladebild festgelegt, das während des Ladens verwendet werden soll (das loading_animation-Drawable). Der Code legt auch ein Bild fest, das verwendet werden soll, wenn das Laden des Bildes fehlschlägt (das broken_image-Drawable). Die vollständige bindImage()-Methode sieht jetzt so aus:
@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. Führen Sie die App aus. Je nach Geschwindigkeit Ihrer Netzwerkverbindung wird möglicherweise kurz das Ladebild angezeigt, während Glide das Immobilienbild herunterlädt und anzeigt. Das Symbol für das fehlerhafte Bild wird jedoch noch nicht angezeigt, auch wenn Sie das Netzwerk deaktivieren. Das beheben Sie im letzten Teil des Codelabs.

Ihre App lädt jetzt Property-Informationen aus dem Internet. Anhand der Daten aus dem ersten MarsProperty-Listenelement haben Sie im Ansichtsmodell eine LiveData-Property erstellt und die Bild-URL aus den Daten dieser Property verwendet, um ein ImageView zu füllen. Da in Ihrer App aber ein Raster mit Bildern angezeigt werden soll, müssen Sie ein RecyclerView mit einem GridLayoutManager verwenden.

Schritt 1: View-Modell aktualisieren

Das View-Modell hat derzeit ein _property LiveData, das ein MarsProperty-Objekt enthält – das erste in der Antwortliste des Webdiensts. In diesem Schritt ändern Sie LiveData so, dass es die gesamte Liste der MarsProperty-Objekte enthält.

  1. Öffnen Sie overview/OverviewViewModel.kt.
  2. Ändern Sie die private Variable _property in _properties. Ändern Sie den Typ in eine Liste von MarsProperty-Objekten.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. Ersetzen Sie die externen property-Live-Daten durch properties. Fügen Sie die Liste auch hier dem Typ LiveData hinzu:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. Scrollen Sie nach unten zur Methode getMarsRealEstateProperties(). Ersetzen Sie im try {}-Block den gesamten Test, den Sie in der vorherigen Aufgabe hinzugefügt haben, durch die unten gezeigte Zeile. Da die Variable listResult eine Liste von MarsProperty-Objekten enthält, können Sie sie einfach _properties.value zuweisen, anstatt auf eine erfolgreiche Antwort zu testen.
_properties.value = listResult

Der gesamte try/catch-Block sieht jetzt so aus:

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

Schritt 2: Layouts und Fragmente aktualisieren

Im nächsten Schritt ändern Sie das Layout und die Fragmente der App, sodass anstelle der einzelnen Bildansicht eine Recycler-Ansicht und ein Rasterlayout verwendet werden.

  1. Öffnen Sie res/layout/gridview_item.xml. Ändern Sie die Datenbindung von OverviewViewModel in MarsProperty und benennen Sie die Variable in "property" um.
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. Ändern Sie im <ImageView> das Attribut app:imageUrl so, dass es auf die Bild-URL im MarsProperty-Objekt verweist:
app:imageUrl="@{property.imgSrcUrl}"
  1. Öffnen Sie overview/OverviewFragment.kt. Entfernen Sie in onCreateview() die Kommentarzeichen aus der Zeile, in der FragmentOverviewBinding aufgebläht wird. Löschen Sie die Zeile, in der GridViewBinding aufgebläht wird, oder kommentieren Sie sie aus. Durch diese Änderungen werden die temporären Änderungen rückgängig gemacht, die Sie in der letzten Aufgabe vorgenommen haben.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. Öffnen Sie res/layout/fragment_overview.xml. Löschen Sie das gesamte <TextView>-Element.
  2. Fügen Sie stattdessen dieses <RecyclerView>-Element hinzu, das ein GridLayoutManager und das grid_view_item-Layout für ein einzelnes Element verwendet:
<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" />

Schritt 3: Fotogrid-Adapter hinzufügen

Das fragment_overview-Layout hat jetzt ein RecyclerView, während das grid_view_item-Layout ein einzelnes ImageView hat. In diesem Schritt binden Sie die Daten über einen RecyclerView-Adapter an RecyclerView.

  1. Öffnen Sie overview/PhotoGridAdapter.kt.
  2. Erstellen Sie die PhotoGridAdapter-Klasse mit den unten gezeigten Konstruktorparametern. Die Klasse PhotoGridAdapter wird von ListAdapter abgeleitet. Der Konstruktor von ListAdapter benötigt den Listenelementtyp, den View-Holder und eine DiffUtil.ItemCallback-Implementierung.

    Importieren Sie die Klassen androidx.recyclerview.widget.ListAdapter und com.example.android.marsrealestate.network.MarsProperty, wenn Sie dazu aufgefordert werden. In den folgenden Schritten implementieren Sie die anderen fehlenden Teile dieses Konstruktors, die Fehler verursachen.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. Klicken Sie auf eine beliebige Stelle in der Klasse PhotoGridAdapter und drücken Sie Control+i, um die ListAdapter-Methoden onCreateViewHolder() und onBindViewHolder() zu implementieren.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. Fügen Sie am Ende der PhotoGridAdapter-Klassendefinition nach den Methoden, die Sie gerade hinzugefügt haben, eine Companion-Objektdefinition für DiffCallback ein, wie unten dargestellt.

    Importieren Sie androidx.recyclerview.widget.DiffUtil, wenn Sie dazu aufgefordert werden.

    Das DiffCallback-Objekt erweitert DiffUtil.ItemCallback mit dem Typ des Objekts, das Sie vergleichen möchten: MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Drücken Sie Control+i, um die Vergleichsmethoden für dieses Objekt zu implementieren, nämlich areItemsTheSame() und areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. Entfernen Sie für die Methode areItemsTheSame() die TODO-Anweisung. Verwenden Sie den referenziellen Gleichheitsoperator von Kotlin (===), der true zurückgibt, wenn die Objektreferenzen für oldItem und newItem identisch sind.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. Verwenden Sie für areContentsTheSame() den Standardgleichheitsoperator nur für die ID von oldItem und newItem.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. Fügen Sie in der PhotoGridAdapter-Klasse unter dem Companion-Objekt eine innere Klassendefinition für MarsPropertyViewHolder hinzu, die RecyclerView.ViewHolder erweitert.

    Importieren Sie androidx.recyclerview.widget.RecyclerView und com.example.android.marsrealestate.databinding.GridViewItemBinding, wenn Sie dazu aufgefordert werden.

    Sie benötigen die GridViewItemBinding-Variable, um MarsProperty an das Layout zu binden. Übergeben Sie die Variable also an MarsPropertyViewHolder. Da die Basisklasse ViewHolder eine Ansicht in ihrem Konstruktor erfordert, übergeben Sie ihr die Bindungs-Root-Ansicht.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. Erstellen Sie in MarsPropertyViewHolder eine bind()-Methode, die ein MarsProperty-Objekt als Argument akzeptiert und binding.property auf dieses Objekt festlegt. Rufen Sie executePendingBindings() auf, nachdem Sie das Attribut festgelegt haben. Dadurch wird das Update sofort ausgeführt.
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. Entfernen Sie in onCreateViewHolder() das TODO und fügen Sie die unten gezeigte Zeile hinzu. Importieren Sie android.view.LayoutInflater, wenn Sie dazu aufgefordert werden.

    Die Methode onCreateViewHolder() muss ein neues MarsPropertyViewHolder zurückgeben, das durch Aufblähen des GridViewItemBinding und Verwenden des LayoutInflater aus dem übergeordneten ViewGroup-Kontext erstellt wird.
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. Entfernen Sie in der Methode onBindViewHolder() das TODO und fügen Sie die unten gezeigten Zeilen hinzu. Hier rufen Sie getItem() auf, um das MarsProperty-Objekt abzurufen, das mit der aktuellen RecyclerView-Position verknüpft ist, und übergeben diese Property dann an die bind()-Methode in der MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

Schritt 4: Binding-Adapter hinzufügen und Teile verbinden

Verwenden Sie schließlich ein BindingAdapter, um das PhotoGridAdapter mit der Liste der MarsProperty-Objekte zu initialisieren. Wenn Sie ein BindingAdapter verwenden, um die RecyclerView-Daten festzulegen, wird die Datenbindung automatisch für die Liste der MarsProperty-Objekte beobachtet.LiveData Der Binding-Adapter wird dann automatisch aufgerufen, wenn sich die Liste MarsProperty ändert.

  1. Öffnen Sie BindingAdapters.kt.
  2. Fügen Sie am Ende der Datei eine bindRecyclerView()-Methode hinzu, die ein RecyclerView und eine Liste von MarsProperty-Objekten als Argumente verwendet. Fügen Sie der Methode eine Annotation mit @BindingAdapter hinzu.

    Importieren Sie androidx.recyclerview.widget.RecyclerView und com.example.android.marsrealestate.network.MarsProperty, wenn Sie dazu aufgefordert werden.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. Wandeln Sie recyclerView.adapter in der Funktion bindRecyclerView() in PhotoGridAdapter um und rufen Sie adapter.submitList() mit den Daten auf. So wird RecyclerView darüber informiert, wenn eine neue Liste verfügbar ist.

Importieren Sie com.example.android.marsrealestate.overview.PhotoGridAdapter, wenn Sie dazu aufgefordert werden.

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. Öffnen Sie res/layout/fragment_overview.xml. Fügen Sie dem Element RecyclerView das Attribut app:listData hinzu und legen Sie es mithilfe der Datenbindung auf viewmodel.properties fest.
app:listData="@{viewModel.properties}"
  1. Öffnen Sie overview/OverviewFragment.kt. Initialisieren Sie in onCreateView(), kurz vor dem Aufruf von setHasOptionsMenu(), den RecyclerView-Adapter in binding.photosGrid mit einem neuen PhotoGridAdapter-Objekt.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. Führen Sie die App aus. Sie sollten ein Raster mit MarsProperty-Bildern sehen. Wenn Sie scrollen, um neue Bilder zu sehen, wird in der App das Symbol für den Ladestatus angezeigt, bevor das Bild selbst eingeblendet wird. Wenn Sie den Flugmodus aktivieren, werden Bilder, die noch nicht geladen wurden, als Symbol für ein defektes Bild angezeigt.

In der MarsRealEstate-App wird das Symbol für ein beschädigtes Bild angezeigt, wenn ein Bild nicht abgerufen werden kann. Wenn jedoch kein Netzwerk vorhanden ist, wird in der App ein leerer Bildschirm angezeigt.

Das ist nicht gerade nutzerfreundlich. In dieser Aufgabe fügen Sie eine grundlegende Fehlerbehandlung hinzu, damit der Nutzer besser nachvollziehen kann, was passiert. Wenn keine Internetverbindung besteht, wird in der App das Symbol für Verbindungsfehler angezeigt. Während die App die Liste MarsProperty abruft, wird die Ladeanimation angezeigt.

Schritt 1: Status zum Ansichtsmodell hinzufügen

Erstellen Sie zuerst ein LiveData im Ansichtsmodell, um den Status der Webanfrage darzustellen. Es gibt drei Status: „Wird geladen“, „Erfolg“ und „Fehler“. Der Ladestatus wird angezeigt, während Sie auf Daten im Aufruf an await() warten.

  1. Öffnen Sie overview/OverviewViewModel.kt. Fügen Sie oben in der Datei (nach den Importen, vor der Klassendefinition) ein enum hinzu, um alle verfügbaren Status darzustellen:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. Benennen Sie sowohl die internen als auch die externen _response-Livedatendefinitionen in der gesamten OverviewViewModel-Klasse in _status um. Da Sie in diesem Codelab bereits Unterstützung für _properties LiveData hinzugefügt haben, wurde die vollständige Webdienstantwort nicht verwendet. Sie benötigen hier eine LiveData, um den aktuellen Status im Blick zu behalten. Sie können die vorhandenen Variablen also einfach umbenennen.

Ändern Sie außerdem die Typen von String in MarsApiStatus..

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. Scrollen Sie nach unten zur Methode getMarsRealEstateProperties() und aktualisieren Sie _response auch hier auf _status. Ändern Sie den String "Success" in den Status MarsApiStatus.DONE und den String "Failure" in MarsApiStatus.ERROR.
  2. Fügen Sie oben im try {}-Block vor dem Aufruf von await() den Status MarsApiStatus.LOADING hinzu. Dies ist der ursprüngliche Status, während die Coroutine ausgeführt wird und Sie auf Daten warten. Der vollständige try/catch {}-Block sieht jetzt so aus:
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. Legen Sie nach dem Fehlerstatus im Block catch {} die _properties LiveData auf eine leere Liste fest. Dadurch wird die RecyclerView gelöscht.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

Schritt 2: Bindungsadapter für die Status-ImageView hinzufügen

Jetzt haben Sie einen Status im Ansichtsmodell, aber er besteht nur aus einer Reihe von Status. Wie kann ich es in der App selbst anzeigen lassen? In diesem Schritt verwenden Sie ein ImageView, das mit der Datenbindung verbunden ist, um Symbole für die Lade- und Fehlerstatus anzuzeigen. Wenn sich die App im Lade- oder Fehlerstatus befindet, sollte ImageView sichtbar sein. Wenn die App geladen wurde, sollte ImageView nicht mehr zu sehen sein.

  1. Öffnen Sie BindingAdapters.kt. Fügen Sie einen neuen Binding-Adapter namens bindStatus() hinzu, der einen ImageView- und einen MarsApiStatus-Wert als Argumente akzeptiert. Importieren Sie com.example.android.marsrealestate.overview.MarsApiStatus, wenn Sie dazu aufgefordert werden.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. Fügen Sie in der Methode bindStatus() ein when {} hinzu, um zwischen den verschiedenen Status zu wechseln.
when (status) {

}
  1. Fügen Sie im when {} einen Case für den Ladestatus (MarsApiStatus.LOADING) hinzu. Legen Sie für diesen Status ImageView auf „Sichtbar“ fest und weisen Sie ihm die Ladeanimation zu. Das ist dasselbe Animations-Drawable, das Sie in der vorherigen Aufgabe für Glide verwendet haben. Importieren Sie android.view.View, wenn Sie dazu aufgefordert werden.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. Fügen Sie einen Fall für den Fehlerstatus hinzu, der MarsApiStatus.ERROR ist. Ähnlich wie beim Status LOADING legen Sie den Status ImageView auf „visible“ fest und verwenden Sie das Drawable für Verbindungsfehler wieder.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. Fügen Sie einen Fall für den Status „Erledigt“ hinzu, der MarsApiStatus.DONE ist. Hier sehen Sie eine erfolgreiche Antwort. Deaktivieren Sie die Sichtbarkeit des Status ImageView, um ihn auszublenden.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Schritt 3: Status-ImageView dem Layout hinzufügen

  1. Öffnen Sie res/layout/fragment_overview.xml. Fügen Sie unter dem RecyclerView-Element innerhalb von ConstraintLayout das unten gezeigte ImageView ein.

    Dieses ImageView hat dieselben Einschränkungen wie das RecyclerView. Bei der Breite und Höhe wird das Bild jedoch mit wrap_content zentriert, anstatt es so zu strecken, dass es die Ansicht ausfüllt. Beachten Sie auch das Attribut app:marsApiStatus, mit dem die Ansicht Ihre BindingAdapter aufruft, wenn sich die Status-Property im Ansichtsmodell ändert.
<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. Aktivieren Sie den Flugmodus in Ihrem Emulator oder auf Ihrem Gerät, um eine fehlende Netzwerkverbindung zu simulieren. Kompilieren Sie die App und führen Sie sie aus. Das Fehlerbild wird angezeigt:

  1. Tippen Sie auf die Zurück-Schaltfläche, um die App zu schließen, und deaktivieren Sie den Flugmodus. Rufen Sie die App über den Bildschirm „Zuletzt verwendet“ wieder auf. Je nach Geschwindigkeit Ihrer Netzwerkverbindung wird möglicherweise kurz ein Ladesymbol angezeigt, während die App den Webdienst abfragt, bevor die Bilder geladen werden.

Android Studio-Projekt: MarsRealEstateGrid

  • Um die Verwaltung von Bildern zu vereinfachen, können Sie die Glide-Bibliothek verwenden, um Bilder in Ihrer App herunterzuladen, zu puffern, zu decodieren und zu cachen.
  • Glide benötigt zwei Dinge, um ein Bild aus dem Internet zu laden: die URL eines Bildes und ein ImageView-Objekt, in das das Bild eingefügt werden soll. Verwenden Sie die Methoden load() und into() mit Glide, um diese Optionen anzugeben.
  • Binding-Adapter sind Erweiterungsmethoden, die zwischen einer Ansicht und den gebundenen Daten dieser Ansicht liegen. Bindungsadapter bieten benutzerdefiniertes Verhalten, wenn sich die Daten ändern, z. B. um Glide aufzurufen, damit ein Bild aus einer URL in ein ImageView geladen wird.
  • Binding-Adapter sind Erweiterungsmethoden, die mit der Annotation @BindingAdapter annotiert sind.
  • Verwenden Sie die Methode apply(), um der Glide-Anfrage Optionen hinzuzufügen. Verwenden Sie beispielsweise apply() mit placeholder(), um ein Drawable für das Laden anzugeben, und apply() mit error(), um ein Fehler-Drawable anzugeben.
  • Um ein Raster aus Bildern zu erstellen, verwenden Sie ein RecyclerView mit einem GridLayoutManager.
  • Verwenden Sie einen Binding-Adapter zwischen RecyclerView und dem Layout, um die Liste der Eigenschaften zu aktualisieren, wenn sie sich ändert.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Sonstiges:

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler und Studenten aufgeführt, die dieses Codelab im Rahmen eines von einem Kursleiter geleiteten Kurses durcharbeiten. Es liegt in der Verantwortung des Kursleiters, Folgendes zu tun:

  • Weisen Sie bei Bedarf Aufgaben zu.
  • Teilen Sie den Schülern/Studenten mit, wie sie Hausaufgaben abgeben können.
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge nach Belieben nutzen und auch andere Hausaufgaben zuweisen, die sie für angemessen halten.

Wenn Sie dieses Codelab selbst durcharbeiten, können Sie mit diesen Hausaufgaben Ihr Wissen testen.

Beantworten Sie diese Fragen

Frage 1

Mit welcher Glide-Methode geben Sie das ImageView an, das das geladene Bild enthalten soll?

▢ into()

▢ with()

▢ imageview()

▢ apply()

Frage 2

Wie gebe ich ein Platzhalterbild an, das während des Ladens von Glide angezeigt werden soll?

▢ Verwenden Sie die into()-Methode mit einem Drawable.

▢ Verwenden Sie RequestOptions() und rufen Sie die Methode placeholder() mit einem Drawable auf.

▢ Weisen Sie der Eigenschaft Glide.placeholder eine Zeichnung zu.

▢ Verwenden Sie RequestOptions() und rufen Sie die Methode loadingImage() mit einem Drawable auf.

Frage 3

Wie geben Sie an, dass eine Methode ein Binding-Adapter ist?

▢ Rufen Sie die Methode setBindingAdapter() für LiveData auf.

▢ Fügen Sie die Methode in eine Kotlin-Datei namens BindingAdapters.kt ein.

▢ Verwenden Sie das Attribut android:adapter im XML-Layout.

▢ Kommentieren Sie die Methode mit @BindingAdapter.

Nächste Lektion: 8.3 Filter und Detailansichten mit Internetdaten

Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Android Kotlin Fundamentals-Codelabs.