Grundlagen von Android und Kotlin 08.1: Daten aus dem Internet abrufen

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

Fast jede Android-App, die Sie entwickeln, muss irgendwann eine Verbindung zum Internet herstellen. In diesem und den folgenden Codelabs erstellen Sie eine App, die eine Verbindung zu einem Webdienst herstellt, um Daten abzurufen und anzuzeigen. Außerdem bauen Sie auf dem auf, was Sie in früheren Codelabs zu ViewModel, LiveData und RecyclerView gelernt haben.

In diesem Codelab verwenden Sie von der Community entwickelte Bibliotheken, um die Netzwerkschicht zu erstellen. Dadurch wird das Abrufen der Daten und Bilder erheblich vereinfacht und die App entspricht einigen Android-Best Practices, z. B. dem Laden von Bildern in einem Hintergrundthread und dem Zwischenspeichern geladener Bilder. Für die asynchronen oder nicht blockierenden Abschnitte im Code, z. B. für die Kommunikation mit der Webdienstebene, müssen Sie die App so ändern, dass Kotlin-Coroutinen verwendet werden. Sie aktualisieren auch die Benutzeroberfläche der App, wenn das Internet langsam oder nicht verfügbar ist, um den Nutzer darüber zu informieren, was gerade passiert.

Was Sie bereits wissen sollten

  • Fragmente erstellen und verwenden
  • Wie Sie zwischen Fragmenten wechseln und safeArgs verwenden, um Daten zwischen Fragmenten zu übergeben.
  • Verwendung von Architekturkomponenten wie ViewModel, ViewModelProvider.Factory, LiveData und LiveData-Transformationen.
  • Verwendung von Coroutinen für lang andauernde Aufgaben

Lerninhalte

  • Was ein REST-Webdienst ist.
  • Verwenden der Retrofit-Bibliothek, um eine Verbindung zu einem REST-Webdienst im Internet herzustellen und eine Antwort zu erhalten.
  • Die Moshi-Bibliothek wird verwendet, um die JSON-Antwort in ein Datenobjekt zu parsen.

Aufgabe

  • Eine Starter-App so ändern, dass eine Webdienst-API-Anfrage gesendet und die Antwort verarbeitet wird.
  • Implementieren Sie mit der Retrofit-Bibliothek eine Netzwerkschicht für Ihre App.
  • Parsen Sie die JSON-Antwort des Webdienstes mit der Moshi-Bibliothek in die Livedaten Ihrer App.
  • Verwenden Sie die Unterstützung von Retrofit für Coroutinen, um den Code zu vereinfachen.

In diesem Codelab und den folgenden Codelabs arbeiten Sie mit einer Starter-App namens „MarsRealEstate“, in der zum Verkauf stehende Immobilien auf dem Mars angezeigt werden. Diese App stellt eine Verbindung zu einem Webdienst her, um die 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, ist nicht sehr visuell. Sie konzentriert sich auf die Netzwerkschicht der App, um eine Verbindung zum Internet herzustellen und die Rohdaten der Immobilie über einen Webdienst herunterzuladen. Damit die Daten richtig abgerufen und geparst werden, geben Sie einfach die Anzahl der Immobilien auf dem Mars in einer Textansicht aus:

.

Die Architektur der MarsRealEstate-App besteht aus zwei Hauptmodulen:

  • Ein Übersichtsfragment, das ein Raster mit Miniaturansichten von Immobilienbildern enthält, das mit einem RecyclerView erstellt wurde.
  • Ein Detailansichtsfragment mit Informationen zu jeder Property.

Die App hat für jedes Fragment ein ViewModel. In diesem Codelab erstellen Sie eine Ebene für den Netzwerkdienst und die ViewModel kommuniziert direkt mit dieser Netzwerkschicht. Das ist ähnlich wie in früheren Codelabs, als ViewModel mit der Datenbank Room kommunizierte.

Die Übersicht ViewModel ist für den Netzwerkaufruf zum Abrufen der Immobilieninformationen für den Mars verantwortlich. Das Detail ViewModel enthält Details für das einzelne Grundstück auf dem Mars, das im Detailfragment angezeigt wird. Für jedes ViewModel verwenden Sie LiveData mit dem lebenszyklusbezogenen Data Binding, um die App-UI zu aktualisieren, wenn sich die Daten ändern.

Sie verwenden die Navigationskomponente, um zwischen den beiden Fragmenten zu wechseln und die ausgewählte Property als Argument zu übergeben.

In dieser Aufgabe laden Sie die Starter-App für MarsRealEstate herunter und führen sie aus. Außerdem machen Sie sich mit der Struktur des Projekts vertraut.

Schritt 1: Fragments und Navigation kennenlernen

  1. Laden Sie die MarsRealEstate-Starter-App herunter und öffnen Sie sie in Android Studio.
  2. Sehen Sie sich app/java/MainActivity.kt an. Die App verwendet Fragmente für beide Bildschirme. Die einzige Aufgabe für die Aktivität besteht also darin, das Layout der Aktivität zu laden.
  3. Sehen Sie sich app/res/layout/activity_main.xml an. Das Aktivitätslayout ist der Host für die beiden Fragmente, die in der Navigationsdatei definiert sind. In diesem Layout wird ein NavHostFragment und der zugehörige Navigationscontroller mit der nav_graph-Ressource instanziiert.
  4. Öffnen Sie app/res/navigation/nav_graph.xml. Hier sehen Sie die Navigationsbeziehung zwischen den beiden Fragmenten. Der Navigationsgraph StartDestination verweist auf overviewFragment. Das Übersichtsfragment wird also beim Starten der App instanziiert.

Schritt 2: Kotlin-Quelldateien und Data Binding ansehen

  1. Maximieren Sie im Bereich Project (Projekt) app > java. Die MarsRealEstate-App hat drei Paketordner: detail, network und overview. Sie entsprechen den drei Hauptkomponenten Ihrer App: den Übersichts- und Detailfragmenten sowie dem Code für die Netzwerkschicht.
  2. Öffnen Sie app/java/overview/OverviewFragment.kt. Der OverviewFragment initialisiert den OverviewViewModel verzögert. Das bedeutet, dass der OverviewViewModel beim ersten Aufruf erstellt wird.
  3. Sehen Sie sich die Methode onCreateView() an. Mit dieser Methode wird das fragment_overview-Layout mithilfe der Datenbindung aufgeblasen, der Lebenszyklus-Inhaber der Bindung wird auf sich selbst (this) festgelegt und die Variable viewModel im binding-Objekt wird darauf festgelegt. Da wir den Lifecycle-Inhaber festgelegt haben, werden alle LiveData, die im Data Binding verwendet werden, automatisch auf Änderungen überwacht und die Benutzeroberfläche wird entsprechend aktualisiert.
  4. Öffnen Sie app/java/overview/OverviewViewModel. Da die Antwort ein LiveData ist und wir den Lebenszyklus für die Bindungsvariable festgelegt haben, werden alle Änderungen daran in der App-Benutzeroberfläche aktualisiert.
  5. Sehen Sie sich den Block init an. Wenn das ViewModel erstellt wird, wird die Methode getMarsRealEstateProperties() aufgerufen.
  6. Sehen Sie sich die Methode getMarsRealEstateProperties() an. In dieser Starter-App enthält diese Methode eine Platzhalterantwort. Ziel dieses Codelabs ist es, die Antwort LiveData innerhalb von ViewModel mit echten Daten zu aktualisieren, die Sie aus dem Internet abrufen.
  7. Öffnen Sie app/res/layout/fragment_overview.xml. Dies ist das Layout für das Übersichtsfragment, mit dem Sie in diesem Codelab arbeiten. Es enthält die Datenbindung für das ViewModel. Sie importiert OverviewViewModel und bindet dann die Antwort von ViewModel an TextView. In späteren Codelabs ersetzen Sie die Textansicht durch ein Raster mit Bildern in einem RecyclerView.
  8. Kompilieren Sie die App und führen Sie sie aus. In der aktuellen Version der App sehen Sie nur die Startantwort: „Set the Mars API Response here!“ (Lege hier die Mars-API-Antwort fest).

Die Immobilieninformationen von Mars werden als REST-Webdienst auf einem Webserver gespeichert. Webdienste, die die REST-Architektur verwenden, werden mit Standard-Webkomponenten und ‑protokollen erstellt.

Sie stellen eine Anfrage an einen Webdienst auf standardisierte Weise über URIs. Die bekannte Web-URL ist eigentlich ein URI-Typ und beide werden in diesem Kurs synonym verwendet. In der App für diese Lektion rufen Sie beispielsweise alle Daten vom folgenden Server ab:

https://android-kotlin-fun-mars-server.appspot.com

Wenn Sie die folgende URL in Ihren Browser eingeben, erhalten Sie eine Liste aller verfügbaren Immobilien auf dem Mars.

https://android-kotlin-fun-mars-server.appspot.com/realestate

Die Antwort eines Webdienstes ist in der Regel im JSON-Format formatiert, einem Austauschformat für die Darstellung strukturierter Daten. Im nächsten Schritt erfahren Sie mehr über JSON. Kurz gesagt ist ein JSON-Objekt eine Sammlung von Schlüssel/Wert-Paaren, die manchmal auch als Wörterbuch, Hash-Map oder assoziatives Array bezeichnet werden. Eine Sammlung von JSON-Objekten ist ein JSON-Array. Dieses Array erhalten Sie als Antwort von einem Webdienst.

Damit diese Daten in die App gelangen, muss Ihre App eine Netzwerkverbindung herstellen und mit dem Server kommunizieren. Anschließend müssen die Antwortdaten in ein Format empfangen und geparst werden, das die App verwenden kann. In diesem Codelab verwenden Sie eine REST-Clientbibliothek namens Retrofit, um diese Verbindung herzustellen.

Schritt 1: Retrofit-Abhängigkeiten zu Gradle hinzufügen

  1. Öffnen Sie build.gradle (Module: app).
  2. Fügen Sie im Abschnitt dependencies diese Zeilen für die Retrofit-Bibliotheken hinzu:
implementation "com.squareup.retrofit2:retrofit:$version_retrofit"
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"


Beachten Sie, dass die Versionsnummern separat in der Gradle-Datei des Projekts definiert sind. Die erste Abhängigkeit ist für die Retrofit 2-Bibliothek selbst und die zweite für den Retrofit-Skalarkonverter. Mit diesem Konverter kann Retrofit das JSON-Ergebnis als String zurückgeben. Die beiden Bibliotheken arbeiten zusammen.

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

Schritt 2: MarsApiService implementieren

Retrofit erstellt eine Netzwerk-API für die App basierend auf den Inhalten des Webdienstes. Dabei werden Daten vom Webdienst abgerufen und über eine separate Konverterbibliothek weitergeleitet, die weiß, wie die Daten decodiert und in Form von nützlichen Objekten zurückgegeben werden. Retrofit bietet integrierte Unterstützung für gängige Webdatenformate wie XML und JSON. Retrofit erstellt letztendlich den größten Teil der Netzwerkschicht für Sie, einschließlich wichtiger Details wie das Ausführen der Anfragen in Hintergrundthreads.

Die Klasse MarsApiService enthält die Netzwerkschicht für die App. Das ist die API, die Ihr ViewModel für die Kommunikation mit dem Webdienst verwendet. In dieser Klasse implementieren Sie die Retrofit-Dienst-API.

  1. Öffnen Sie app/java/network/MarsApiService.kt. Derzeit enthält die Datei nur eine Konstante für die Basis-URL des Webdienstes:
    .
private const val BASE_URL = 
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. Verwenden Sie direkt unter dieser Konstante einen Retrofit-Builder, um ein Retrofit-Objekt zu erstellen. Importieren Sie retrofit2.Retrofit und retrofit2.converter.scalars.ScalarsConverterFactory, wenn Sie dazu aufgefordert werden.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()

Retrofit benötigt mindestens zwei Dinge, um eine Webdienst-API zu erstellen: die Basis-URI für den Webdienst und eine Converter-Factory. Der Converter teilt Retrofit mit, was mit den Daten geschehen soll, die vom Webdienst zurückgegeben werden. In diesem Fall soll Retrofit eine JSON-Antwort vom Webdienst abrufen und als String zurückgeben. Retrofit hat ein ScalarsConverter, das Strings und andere primitive Typen unterstützt. Sie rufen also addConverterFactory() für den Builder mit einer Instanz von ScalarsConverterFactory auf. Rufen Sie schließlich build() auf, um das Retrofit-Objekt zu erstellen.

  1. Definieren Sie direkt unter dem Aufruf des Retrofit-Builders eine Schnittstelle, die definiert, wie Retrofit über HTTP-Anfragen mit dem Webserver kommuniziert. Importieren Sie retrofit2.http.GET und retrofit2.Call, wenn Sie dazu aufgefordert werden.
interface MarsApiService {
    @GET("realestate")
    fun getProperties():
            Call<String>
}

Derzeit besteht das Ziel darin, den JSON-Antwortstring vom Webdienst abzurufen. Dazu benötigen Sie nur eine Methode: getProperties(). Um Retrofit mitzuteilen, was diese Methode tun soll, verwenden Sie die Annotation @GET und geben Sie den Pfad oder Endpunkt für diese Webdienstmethode an. In diesem Fall heißt der Endpunkt realestate. Wenn die Methode getProperties() aufgerufen wird, hängt Retrofit den Endpunkt realestate an die Basis-URL an (die Sie im Retrofit-Builder definiert haben) und erstellt ein Call-Objekt. Mit diesem Call-Objekt wird die Anfrage gestartet.

  1. Definieren Sie unter der MarsApiService-Schnittstelle ein öffentliches Objekt namens MarsApi, um den Retrofit-Dienst zu initialisieren.
object MarsApi {
    val retrofitService : MarsApiService by lazy { 
       retrofit.create(MarsApiService::class.java) }
}

Mit der Retrofit-Methode create() wird der Retrofit-Dienst selbst mit der MarsApiService-Schnittstelle erstellt. Da dieser Aufruf ressourcenintensiv ist und die App nur eine Retrofit-Dienstinstanz benötigt, machen Sie den Dienst über ein öffentliches Objekt namens MarsApi für den Rest der App verfügbar und initialisieren den Retrofit-Dienst dort verzögert. Nachdem die Einrichtung abgeschlossen ist, erhält Ihre App bei jedem Aufruf von MarsApi.retrofitService ein Singleton-Retrofit-Objekt, das MarsApiService implementiert.

Schritt 3: Webdienst in OverviewViewModel aufrufen

  1. Öffnen Sie app/java/overview/OverviewViewModel.kt. Scrollen Sie nach unten zur Methode getMarsRealEstateProperties().
private fun getMarsRealEstateProperties() {
   _response.value = "Set the Mars API Response here!"
}

In dieser Methode rufen Sie den Retrofit-Dienst auf und verarbeiten den zurückgegebenen JSON-String. Derzeit gibt es nur einen Platzhalterstring für die Antwort.

  1. Löschen Sie die Platzhalterzeile, in der die Antwort auf „Set the Mars API Response here!“ festgelegt ist.
  2. Fügen Sie in getMarsRealEstateProperties() den unten gezeigten Code ein. Importieren Sie retrofit2.Callback und com.example.android.marsrealestate.network.MarsApi, wenn Sie dazu aufgefordert werden.

    Die Methode MarsApi.retrofitService.getProperties() gibt ein Call-Objekt zurück. Anschließend können Sie enqueue() für dieses Objekt aufrufen, um die Netzwerkanfrage in einem Hintergrundthread zu starten.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<String> {
})
  1. Klicken Sie auf das rot unterstrichene Wort object. Wählen Sie Code > Implement methods (Code > Methoden implementieren) aus. Wählen Sie aus der Liste sowohl onResponse() als auch onFailure() aus.


    Android Studio fügt den Code mit TODOs in jeder Methode hinzu:
override fun onFailure(call: Call<String>, t: Throwable) {
       TODO("not implemented") 
}

override fun onResponse(call: Call<String>, 
   response: Response<String>) {
       TODO("not implemented") 
}
  1. Löschen Sie in onFailure() das TODO und legen Sie _response auf eine Fehlermeldung fest, wie unten gezeigt. _response ist ein LiveData-String, der bestimmt, was in der Textansicht angezeigt wird. In jedem Status muss _response LiveData

    aktualisiert werden.Der onFailure()-Callback wird aufgerufen, wenn die Antwort des Webdienstes fehlschlägt. Legen Sie für diese Antwort den _response-Status auf "Failure: " fest, verkettet mit der Nachricht aus dem Throwable-Argument.
override fun onFailure(call: Call<String>, t: Throwable) {
   _response.value = "Failure: " + t.message
}
  1. Löschen Sie in onResponse() das TODO und legen Sie _response auf den Antworttext fest. Der onResponse()-Callback wird aufgerufen, wenn die Anfrage erfolgreich ist und der Webdienst eine Antwort zurückgibt.
override fun onResponse(call: Call<String>, 
   response: Response<String>) {
      _response.value = response.body()
}

Schritt 4: Internetberechtigung definieren

  1. Kompiliere und führe die MarsRealEstate-App aus. Die App wird sofort mit einem Fehler geschlossen.
  2. Klicken Sie in Android Studio auf den Tab Logcat und notieren Sie sich den Fehler im Log. Er beginnt mit einer Zeile wie dieser:
Process: com.example.android.marsrealestate, PID: 10646
java.lang.SecurityException: Permission denied (missing INTERNET permission?)

Die Fehlermeldung weist darauf hin, dass Ihrer App möglicherweise die Berechtigung INTERNET fehlt. Die Verbindung zum Internet birgt Sicherheitsrisiken. Daher haben Apps standardmäßig keine Internetverbindung. Sie müssen Android explizit mitteilen, dass die App Zugriff auf das Internet benötigt.

  1. Öffnen Sie app/manifests/AndroidManifest.xml. Fügen Sie diese Zeile direkt vor dem <application>-Tag ein:
<uses-permission android:name="android.permission.INTERNET" />
  1. Kompilieren Sie die App und führen Sie sie noch einmal aus. Wenn alles mit Ihrer Internetverbindung funktioniert, sehen Sie JSON-Text mit Mars Property-Daten.
  2. Tippen Sie auf Ihrem Gerät oder im Emulator auf die Schaltfläche Zurück, um die App zu schließen.
  3. Versetzen Sie Ihr Gerät oder Ihren Emulator in den Flugmodus und öffnen Sie die App dann über das Menü „Zuletzt verwendet“ noch einmal oder starten Sie die App in Android Studio neu.


  1. Deaktivieren Sie den Flugmodus wieder.

Sie erhalten jetzt eine JSON-Antwort vom Mars-Webdienst. Das ist schon mal ein guter Anfang. Sie benötigen jedoch Kotlin-Objekte und keinen großen JSON-String. Es gibt eine Bibliothek namens Moshi, die ein Android-JSON-Parser ist, der einen JSON-String in Kotlin-Objekte konvertiert. Retrofit hat einen Converter, der mit Moshi funktioniert. Das ist also eine gute Bibliothek für Ihre Zwecke.

In dieser Aufgabe verwenden Sie die Moshi-Bibliothek mit Retrofit, um die JSON-Antwort des Webdienstes in nützliche Kotlin-Objekte für Mars Property zu parsen. Sie ändern die App so, dass anstelle des Roh-JSON die Anzahl der zurückgegebenen Mars-Eigenschaften angezeigt wird.

Schritt 1: Moshi-Bibliotheksabhängigkeiten hinzufügen

  1. Öffnen Sie build.gradle (Module: app).
  2. Fügen Sie im Bereich „dependencies“ (Abhängigkeiten) den unten gezeigten Code ein, um die Moshi-Abhängigkeiten einzuschließen. Wie bei Retrofit wird $version_moshi separat in der Gradle-Datei auf Projektebene definiert. Diese Abhängigkeiten fügen Unterstützung für die Moshi-Kern-JSON-Bibliothek und für die Kotlin-Unterstützung von Moshi hinzu.
implementation "com.squareup.moshi:moshi:$version_moshi"
implementation "com.squareup.moshi:moshi-kotlin:$version_moshi"
  1. Suchen Sie im dependencies-Block nach der Zeile für den Retrofit-Skalarkonverter:
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"
  1. Ändern Sie die Zeile so, dass converter-moshi verwendet wird:
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
  1. Klicken Sie auf Jetzt synchronisieren, um das Projekt mit den neuen Abhängigkeiten neu zu erstellen.

Schritt 2: MarsProperty-Datenklasse implementieren

Ein Beispiel für einen Eintrag in der JSON-Antwort, die Sie vom Webdienst erhalten, sieht so aus:

[{"price":450000,
"id":"424906",
"type":"rent",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"},
...]

Die oben gezeigte JSON-Antwort ist ein Array, was durch die eckigen Klammern angezeigt wird. Das Array enthält JSON-Objekte, die von geschweiften Klammern umgeben sind. Jedes Objekt enthält eine Reihe von Name-Wert-Paaren, die durch Doppelpunkte getrennt sind. Namen sind in Anführungszeichen eingeschlossen. Werte können Zahlen oder Strings sein. Strings werden in Anführungszeichen gesetzt. Der price für diese Property ist beispielsweise 450.000 $und der img_src ist eine URL, die den Speicherort der Bilddatei auf dem Server angibt.

Im obigen Beispiel hat jeder Eintrag für eine Mars-Eigenschaft die folgenden JSON-Schlüssel/Wert-Paare:

  • price: Der Preis des Mars-Grundstücks als Zahl.
  • id: Die ID der Property als String.
  • type: entweder "rent" oder "buy".
  • img_src: Die URL des Bildes als String.

Moshi parst diese JSON-Daten und konvertiert sie in Kotlin-Objekte. Dazu ist eine Kotlin-Datenklasse zum Speichern der geparsten Ergebnisse erforderlich. Erstellen Sie diese Klasse als Nächstes.

  1. Öffnen Sie app/java/network/MarsProperty.kt.
  2. Ersetzen Sie die vorhandene MarsProperty-Klassendefinition durch den folgenden Code:
data class MarsProperty(
   val id: String, val img_src: String,
   val type: String,
   val price: Double
)

Jede der Variablen in der Klasse MarsProperty entspricht einem Schlüsselnamen im JSON-Objekt. Um die Typen im JSON abzugleichen, verwenden Sie String-Objekte für alle Werte außer price, das ein Double ist. Mit einem Double kann jede JSON-Zahl dargestellt werden.

Wenn Moshi das JSON-Dokument parst, werden die Schlüssel anhand des Namens zugeordnet und die Datenobjekte mit entsprechenden Werten gefüllt.

  1. Ersetzen Sie die Zeile für den img_src-Schlüssel durch die unten gezeigte Zeile. Importieren Sie com.squareup.moshi.Json, wenn Sie dazu aufgefordert werden.
@Json(name = "img_src") val imgSrcUrl: String,

Manchmal können die Schlüsselnamen in einer JSON-Antwort zu verwirrenden Kotlin-Eigenschaften führen oder nicht Ihrem Programmierstil entsprechen. In der JSON-Datei wird beispielsweise für den Schlüssel img_src ein Unterstrich verwendet, während für Kotlin-Eigenschaften in der Regel Groß- und Kleinbuchstaben („Camel Case“) verwendet werden.

Wenn Sie in Ihrer Datenklasse Variablennamen verwenden möchten, die sich von den Schlüsselnamen in der JSON-Antwort unterscheiden, verwenden Sie die Annotation @Json. In diesem Beispiel lautet der Name der Variablen in der Datenklasse imgSrcUrl. Die Variable wird mit @Json(name = "img_src") dem JSON-Attribut img_src zugeordnet.

Schritt 3: MarsApiService und OverviewViewModel aktualisieren

Nachdem Sie die Datenklasse MarsProperty eingerichtet haben, können Sie die Netzwerk-API und ViewModel aktualisieren, um die Moshi-Daten einzubeziehen.

  1. Öffnen Sie network/MarsApiService.kt. Möglicherweise werden Fehler vom Typ „missing-class“ für ScalarsConverterFactory angezeigt. Das liegt an der Änderung der Retrofit-Abhängigkeit, die Sie in Schritt 1 vorgenommen haben. Beheben Sie diese Fehler so bald wie möglich.
  2. Fügen Sie oben in der Datei, direkt vor dem Retrofit-Builder, den folgenden Code hinzu, um die Moshi-Instanz zu erstellen. Importieren Sie com.squareup.moshi.Moshi und com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory, wenn Sie dazu aufgefordert werden.
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

Ähnlich wie bei Retrofit erstellen Sie hier ein moshi-Objekt mit dem Moshi-Builder. Damit die Annotationen von Moshi in Kotlin richtig funktionieren, fügen Sie KotlinJsonAdapterFactory hinzu und rufen Sie dann build() auf.

  1. Ändern Sie den Retrofit-Builder so, dass er MoshiConverterFactory anstelle von ScalarConverterFactory verwendet, und übergeben Sie die moshi-Instanz, die Sie gerade erstellt haben. Importieren Sie retrofit2.converter.moshi.MoshiConverterFactory, wenn Sie dazu aufgefordert werden.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()
  1. Löschen Sie auch den Import für ScalarConverterFactory.

Zu löschender Code:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. Aktualisieren Sie die MarsApiService-Schnittstelle so, dass Retrofit eine Liste von MarsProperty-Objekten zurückgibt, anstatt Call<String>.
    zurückzugeben.
interface MarsApiService {
   @GET("realestate")
   fun getProperties():
      Call<List<MarsProperty>>
}
  1. Öffnen Sie OverviewViewModel.kt. Scrollen Sie in der Methode getMarsRealEstateProperties() nach unten zum Aufruf von getProperties().enqueue().
  2. Ändern Sie das Argument für enqueue() von Callback<String> in Callback<List<MarsProperty>>. Importieren Sie com.example.android.marsrealestate.network.MarsProperty, wenn Sie dazu aufgefordert werden.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<List<MarsProperty>> {
  1. Ändern Sie in onFailure() das Argument von Call<String> in Call<List<MarsProperty>>:
override fun onFailure(call: Call<List<MarsProperty>>, t: Throwable) {
  1. Nehmen Sie dieselbe Änderung an beiden Argumenten für onResponse() vor:
override fun onResponse(call: Call<List<MarsProperty>>, 
   response: Response<List<MarsProperty>>) {
  1. Ersetzen Sie im Text von onResponse() die vorhandene Zuweisung zu _response.value durch die unten gezeigte Zuweisung. Da response.body() jetzt eine Liste von MarsProperty-Objekten ist, entspricht die Größe dieser Liste der Anzahl der geparsten Eigenschaften. In dieser Antwortnachricht wird die Anzahl der Properties ausgegeben:
_response.value = 
   "Success: ${response.body()?.size} Mars properties retrieved"
  1. Achten Sie darauf, dass der Flugmodus deaktiviert ist. Kompilieren und führen Sie die App aus. Dieses Mal sollte in der Meldung die Anzahl der vom Webdienst zurückgegebenen Attribute angezeigt werden:

Der Retrofit-API-Dienst wird jetzt ausgeführt, verwendet aber einen Callback mit zwei Callback-Methoden, die Sie implementieren mussten. Eine Methode verarbeitet den Erfolg und eine andere den Fehler. Das Fehlerergebnis meldet Ausnahmen. Ihr Code wäre effizienter und leichter lesbar, wenn Sie anstelle von Callbacks Coroutinen mit Ausnahmebehandlung verwenden könnten. Praktischerweise gibt es für Retrofit eine Bibliothek, die Coroutinen integriert.

In dieser Aufgabe konvertieren Sie Ihren Netzwerkdienst und ViewModel, sodass sie Coroutinen verwenden.

Schritt 1: Coroutine-Abhängigkeiten hinzufügen

  1. Öffnen Sie build.gradle (Module: app).
  2. Fügen Sie im Abschnitt „dependencies“ Unterstützung für die Kotlin-Core-Coroutine-Bibliotheken und die Retrofit-Coroutine-Bibliothek hinzu:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines"

implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter"
  1. Klicken Sie auf Jetzt synchronisieren, um das Projekt mit den neuen Abhängigkeiten neu zu erstellen.

Schritt 2: MarsApiService und OverviewViewModel aktualisieren

  1. Aktualisieren Sie in MarsApiService.kt den Retrofit-Builder, damit er CoroutineCallAdapterFactory verwendet. Der vollständige Builder sieht jetzt so aus:
private val retrofit = Retrofit.Builder()
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .baseUrl(BASE_URL)
        .build()

Mit Call-Adaptern kann Retrofit APIs erstellen, die etwas anderes als die Standardklasse Call zurückgeben. In diesem Fall können wir mit CoroutineCallAdapterFactory das Call-Objekt, das von getProperties() zurückgegeben wird, durch ein Deferred-Objekt ersetzen.

  1. Ändern Sie in der Methode getProperties() Call<List<MarsProperty>> in Deferred<List<MarsProperty>>. Importieren Sie kotlinx.coroutines.Deferred, wenn Sie dazu aufgefordert werden. Die vollständige getProperties()-Methode sieht so aus:
@GET("realestate")
fun getProperties():
   Deferred<List<MarsProperty>>

Die Deferred-Schnittstelle definiert einen Coroutine-Job, der einen Ergebniswert zurückgibt (Deferred erbt von Job). Die Deferred-Schnittstelle enthält eine Methode namens await(), die bewirkt, dass Ihr Code wartet, ohne zu blockieren, bis der Wert bereit ist. Anschließend wird dieser Wert zurückgegeben.

  1. Öffnen Sie OverviewViewModel.kt. Fügen Sie direkt vor dem init-Block einen Coroutine-Job hinzu:
private var viewModelJob = Job()
  1. Erstellen Sie mit dem Haupt-Dispatcher einen Coroutine-Scope für diesen neuen Job:
private val coroutineScope = CoroutineScope(
   viewModelJob + Dispatchers.Main )

Der Dispatchers.Main-Dispatcher verwendet den UI-Thread für seine Arbeit. Da Retrofit alle Aufgaben in einem Hintergrundthread ausführt, gibt es keinen Grund, einen anderen Thread für den Bereich zu verwenden. So können Sie den Wert von MutableLiveData ganz einfach aktualisieren, wenn Sie ein Ergebnis erhalten.

  1. Löschen Sie den gesamten Code in getMarsRealEstateProperties(). Hier verwenden Sie Coroutinen anstelle des Aufrufs von enqueue() und der Callbacks onFailure() und onResponse().
  2. Starten Sie die Coroutine in getMarsRealEstateProperties():
coroutineScope.launch { 

}


Damit Sie das Deferred-Objekt verwenden können, das Retrofit für die Netzwerkaufgabe zurückgibt, müssen Sie sich in einer Coroutine befinden. Daher starten Sie hier die gerade erstellte Coroutine. Sie führen weiterhin Code im Hauptthread aus, lassen die Nebenläufigkeit aber von Coroutinen verwalten.

  1. Rufen Sie im Startblock getProperties() für das retrofitService-Objekt auf:
var getPropertiesDeferred = MarsApi.retrofitService.getProperties()

Wenn Sie getProperties() über den MarsApi-Dienst aufrufen, wird der Netzwerkaufruf in einem Hintergrundthread erstellt und gestartet. Das Deferred-Objekt für diese Aufgabe wird zurückgegeben.

  1. Fügen Sie außerdem im Startblock einen try-/catch-Block hinzu, um Ausnahmen zu verarbeiten:
try {

} catch (e: Exception) {
  
}
  1. Rufen Sie im Block try {} die Funktion await() für das Objekt Deferred auf:
var listResult = getPropertiesDeferred.await()

Wenn Sie await() für das Deferred-Objekt aufrufen, wird das Ergebnis des Netzwerkaufrufs zurückgegeben, sobald der Wert bereit ist. Die Methode await() ist nicht blockierend. Der Mars API-Dienst ruft die Daten also aus dem Netzwerk ab, ohne den aktuellen Thread zu blockieren. Das ist wichtig, da wir uns im Bereich des UI-Threads befinden. Sobald die Aufgabe abgeschlossen ist, wird Ihr Code an der Stelle fortgesetzt, an der er unterbrochen wurde. Das geschieht innerhalb des try {}, sodass Sie Ausnahmen abfangen können.

  1. Aktualisieren Sie auch im try {}-Block nach der await()-Methode die Antwortnachricht für die erfolgreiche Antwort:
_response.value = 
   "Success: ${listResult.size} Mars properties retrieved"
  1. Verarbeiten Sie im Block catch {} die Fehlerantwort:
_response.value = "Failure: ${e.message}"


Die vollständige getMarsRealEstateProperties()-Methode sieht jetzt so aus:

private fun getMarsRealEstateProperties() {
   coroutineScope.launch {
       var getPropertiesDeferred = 
          MarsApi.retrofitService.getProperties()
       try {          
           _response.value = 
              "Success: ${listResult.size} Mars properties retrieved"
       } catch (e: Exception) {
           _response.value = "Failure: ${e.message}"
       }
   }
}
  1. Fügen Sie unten in der Klasse einen onCleared()-Callback mit diesem Code hinzu:
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}

Das Laden von Daten sollte beendet werden, wenn das ViewModel zerstört wird, da das OverviewFragment, das dieses ViewModel verwendet, dann nicht mehr vorhanden ist. Wenn das Laden beendet werden soll, wenn ViewModel zerstört wird, überschreiben Sie onCleared(), um den Job abzubrechen.

  1. Kompilieren und führen Sie die App aus. Sie erhalten dasselbe Ergebnis wie in der vorherigen Aufgabe (einen Bericht über die Anzahl der Properties), aber mit einfacherem Code und einer besseren Fehlerbehandlung.

Android Studio-Projekt: MarsRealEstateNetwork

REST-Webdienste

  • Ein Webdienst ist ein Dienst im Internet, über den Ihre App Anfragen stellen und Daten zurückerhalten kann.
  • Gängige Webservices verwenden eine REST-Architektur. Webdienste, die eine REST-Architektur bieten, werden als RESTful-Dienste bezeichnet. RESTful-Webdienste werden mit Standard-Webkomponenten und ‑Protokollen erstellt.
  • Sie stellen Anfragen an einen REST-Webdienst auf standardisierte Weise über URIs.
  • Damit eine App einen Webdienst nutzen kann, muss sie eine Netzwerkverbindung herstellen und mit dem Dienst kommunizieren. Anschließend muss die App die Antwortdaten empfangen und in ein Format parsen, das die App verwenden kann.
  • Die Retrofit-Bibliothek ist eine Clientbibliothek, mit der Ihre App Anfragen an einen REST-Webdienst senden kann.
  • Mit Konvertern können Sie Retrofit mitteilen, was mit Daten geschehen soll, die an den Webdienst gesendet und vom Webdienst zurückgegeben werden. Der ScalarsConverter-Konverter behandelt die Webdienstdaten beispielsweise als String oder ein anderes Primitiv.
  • Damit Ihre App Verbindungen zum Internet herstellen kann, fügen Sie die Berechtigung "android.permission.INTERNET" im Android-Manifest hinzu.

JSON-Parsing

  • Die Antwort eines Webdienstes ist oft im JSON-Format formatiert, einem gängigen Austauschformat für die Darstellung strukturierter Daten.
  • Ein JSON-Objekt ist eine Sammlung von Schlüssel/Wert-Paaren. Diese Sammlung wird manchmal auch als Dictionary, Hash Map oder assoziatives Array bezeichnet.
  • Eine Sammlung von JSON-Objekten ist ein JSON-Array. Sie erhalten ein JSON-Array als Antwort von einem Webdienst.
  • Die Schlüssel in einem Schlüssel/Wert-Paar sind in Anführungszeichen eingeschlossen. Die Werte können Zahlen oder Strings sein. Strings werden ebenfalls in Anführungszeichen gesetzt.
  • Die Moshi-Bibliothek ist ein Android-JSON-Parser, der einen JSON-String in Kotlin-Objekte konvertiert. Retrofit hat einen Converter, der mit Moshi funktioniert.
  • Moshi vergleicht die Schlüssel in einer JSON-Antwort mit Attributen in einem Datenobjekt, die denselben Namen haben.
  • Wenn Sie für einen Schlüssel einen anderen Attributnamen verwenden möchten, versehen Sie das Attribut mit der Annotation @Json und dem JSON-Schlüsselnamen.

Retrofit und Coroutinen

  • Mit Call-Adaptern kann Retrofit APIs erstellen, die etwas anderes als die Standardklasse Call zurückgeben. Verwenden Sie die Klasse CoroutineCallAdapterFactory, um Call durch eine Coroutine Deferred zu ersetzen.
  • Verwenden Sie die Methode await() für das Objekt Deferred, damit Ihr Coroutinen-Code wartet, ohne zu blockieren, bis der Wert bereit ist. Anschließend wird der Wert zurückgegeben.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Kotlin-Dokumentation:

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

Was sind die beiden wichtigsten Dinge, die Retrofit zum Erstellen einer Webdienst-API benötigt?

▢ Der Basis-URI für den Webdienst und eine GET-Abfrage.

▢ Die Basis-URI für den Webdienst und eine Converter-Factory.

▢ Eine Netzwerkverbindung zum Webdienst und ein Autorisierungstoken.

▢ Eine Converter-Factory und ein Parser für die Antwort.

Frage 2

Was ist der Zweck der Moshi-Bibliothek?

▢ Daten von einem Webdienst abrufen.

▢ Um mit Retrofit zu interagieren und eine Webdienstanfrage zu stellen.

▢ Eine JSON-Antwort von einem Webdienst in Kotlin-Datenobjekte parsen.

▢ Kotlin-Objekte umbenennen, damit sie mit den Schlüsseln in der JSON-Antwort übereinstimmen.

Frage 3

Wofür werden Retrofit-Call-Adapter verwendet?

▢ Sie ermöglichen die Verwendung von Koroutinen in Retrofit.

▢ Sie passen die Webdienstantwort an Kotlin-Datenobjekte an.

▢ Sie ändern einen Retrofit-Aufruf in einen Webdienstaufruf.

▢ Sie ermöglichen es, in Retrofit etwas anderes als die Standardklasse Call zurückzugeben.

Nächste Lektion starten: 8.2 Bilder aus dem Internet laden und anzeigen

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