Grundlagen von Android und Kotlin 05.1: ViewModel und ViewModelFactory

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.

Titelbildschirm

Spielbildschirm

Bewertungsbildschirm

Einführung

In diesem Codelab lernen Sie eine der Android-Architekturkomponenten kennen: ViewModel.

  • Mit der Klasse ViewModel können Sie UI-bezogene Daten auf lebenszyklusbewusste Weise speichern und verwalten. Mit der Klasse ViewModel können Daten Änderungen der Gerätekonfiguration wie Bildschirmdrehungen und Änderungen der Tastaturverfügbarkeit überdauern.
  • Mit der Klasse ViewModelFactory instanziieren und geben Sie das ViewModel-Objekt zurück, das Konfigurationsänderungen übersteht.

Was Sie bereits wissen sollten

  • So erstellen Sie grundlegende Android-Apps in Kotlin.
  • So verwenden Sie den Navigationsgraphen, um die Navigation in Ihrer App zu implementieren.
  • Code hinzufügen, um zwischen den Zielen Ihrer App zu wechseln und Daten zwischen Navigationszielen zu übergeben
  • Funktionsweise des Aktivitäts- und Fragmentlebenszyklus.
  • Informationen dazu, wie Sie einer App Logging-Informationen hinzufügen und Logs mit Logcat in Android Studio lesen.

Lerninhalte

Aufgaben

  • Fügen Sie der App ein ViewModel hinzu, um die Daten der App zu speichern, damit sie Konfigurationsänderungen überstehen.
  • Verwenden Sie ViewModelFactory und das Factory-Method-Entwurfsmuster, um ein ViewModel-Objekt mit Konstruktorparametern zu instanziieren.

In den Codelabs zu Lektion 5 entwickeln Sie die App „GuessTheWord“ auf Grundlage von Startcode. GuessTheWord ist ein Schattenspiel für zwei Spieler, bei dem die Spieler zusammenarbeiten, um die höchstmögliche Punktzahl zu erreichen.

Der erste Spieler sieht sich die Wörter in der App an und stellt sie nacheinander dar, ohne dem zweiten Spieler das Wort zu zeigen. Der zweite Spieler versucht, das Wort zu erraten.

Um das Spiel zu starten, öffnet der erste Spieler die App auf dem Gerät und sieht ein Wort, z. B. „Gitarre“, wie im Screenshot unten zu sehen.

Der erste Spieler stellt das Wort dar, ohne es auszusprechen.

  • Wenn der zweite Spieler das Wort richtig errät, drückt der erste Spieler auf die Schaltfläche Got It (Erraten). Dadurch wird die Anzahl um eins erhöht und das nächste Wort angezeigt.
  • Wenn der zweite Spieler das Wort nicht erraten kann, drückt der erste Spieler die Schaltfläche Überspringen. Dadurch wird die Anzahl um eins verringert und zum nächsten Wort gesprungen.
  • Drücken Sie die Schaltfläche Spiel beenden, um das Spiel zu beenden. (Diese Funktion ist nicht im Startercode für das erste Codelab der Reihe enthalten.)

In dieser Aufgabe laden Sie die Starter-App herunter, führen sie aus und sehen sich den Code an.

Schritt 1: Vorbereitung

  1. Laden Sie den GuessTheWord-Startcode herunter und öffnen Sie das Projekt in Android Studio.
  2. Führen Sie die App auf einem Android-Gerät oder in einem Emulator aus.
  3. Tippe auf die Tasten. Die Schaltfläche Überspringen zeigt das nächste Wort an und verringert die Punktzahl um eins. Die Schaltfläche Verstanden zeigt das nächste Wort an und erhöht die Punktzahl um eins. Die Schaltfläche Spiel beenden ist nicht implementiert. Wenn Sie darauf tippen, passiert also nichts.

Schritt 2: Code durchgehen

  1. Sehen Sie sich den Code in Android Studio an, um ein Gefühl dafür zu bekommen, wie die App funktioniert.
  2. Sehen Sie sich unbedingt die unten beschriebenen Dateien an, die besonders wichtig sind.

MainActivity.kt

Diese Datei enthält nur Standardcode, der durch die Vorlage generiert wurde.

res/layout/main_activity.xml

Diese Datei enthält das Hauptlayout der App. Im NavHostFragment werden die anderen Fragmente gehostet, während der Nutzer durch die App navigiert.

UI-Fragmente

Der Startercode enthält drei Fragmente in drei verschiedenen Paketen unter dem Paket com.example.android.guesstheword.screens:

  • title/TitleFragment für den Titelbildschirm
  • game/GameFragment für den Spielbildschirm
  • score/ScoreFragment für den Bildschirm mit der Punktzahl

screens/title/TitleFragment.kt

Das Titelfragment ist der erste Bildschirm, der beim Starten der App angezeigt wird. Für die Schaltfläche Play (Spielen) wird ein Click-Handler festgelegt, um zum Spielbildschirm zu navigieren.

screens/game/GameFragment.kt

Das ist das Hauptfragment, in dem der Großteil der Spielhandlung stattfindet:

  • Variablen werden für das aktuelle Wort und die aktuelle Punktzahl definiert.
  • Die wordList, die in der Methode resetList() definiert ist, ist eine Beispielliste mit Wörtern, die im Spiel verwendet werden sollen.
  • Die onSkip()-Methode ist der Click-Handler für die Schaltfläche Überspringen. Der Wert wird um 1 verringert und das nächste Wort wird mit der Methode nextWord() angezeigt.
  • Die Methode onCorrect() ist der Klick-Handler für die Schaltfläche Verstanden. Diese Methode wird ähnlich wie die Methode onSkip() implementiert. Der einzige Unterschied besteht darin, dass bei dieser Methode 1 zum Ergebnis addiert wird, anstatt es zu subtrahieren.

screens/score/ScoreFragment.kt

ScoreFragment ist der letzte Bildschirm im Spiel und zeigt die Endpunktzahl des Spielers an. In diesem Codelab fügen Sie die Implementierung hinzu, um diesen Bildschirm und die endgültige Punktzahl anzuzeigen.

res/navigation/main_navigation.xml

Das Navigationsdiagramm zeigt, wie die Fragmente über die Navigation miteinander verbunden sind:

  • Über das Titelfragment kann der Nutzer zum Spielfragment navigieren.
  • Vom Spiel-Fragment aus kann der Nutzer zum Ergebnis-Fragment navigieren.
  • Über das Score-Fragment kann der Nutzer zum Spiel-Fragment zurückkehren.

In dieser Aufgabe suchen Sie nach Problemen in der Starter-App „GuessTheWord“.

  1. Führen Sie den Startercode aus und spielen Sie das Spiel einige Wörter lang. Tippen Sie nach jedem Wort auf Überspringen oder Verstanden.
  2. Auf dem Spielbildschirm werden jetzt ein Wort und der aktuelle Punktestand angezeigt. Ändern Sie die Bildschirmausrichtung, indem Sie das Gerät oder den Emulator drehen. Beachten Sie, dass die aktuelle Punktzahl verloren geht.
  3. Lass das Spiel noch ein paar Wörter durchlaufen. Wenn der Spielbildschirm mit einem Ergebnis angezeigt wird, schließen Sie die App und öffnen Sie sie wieder. Das Spiel wird von Anfang an neu gestartet, da der App-Status nicht gespeichert wird.
  4. Spielen Sie das Spiel einige Wörter lang und tippen Sie dann auf die Schaltfläche Spiel beenden. Es passiert nichts.

Probleme in der App:

  • Die Starter-App speichert und stellt den App-Status bei Konfigurationsänderungen nicht wieder her, z. B. wenn sich die Geräteausrichtung ändert oder die App beendet und neu gestartet wird.
    Sie können dieses Problem mit dem onSaveInstanceState()-Callback beheben. Wenn Sie die onSaveInstanceState()-Methode verwenden, müssen Sie jedoch zusätzlichen Code schreiben, um den Status in einem Bundle zu speichern und die Logik zum Abrufen dieses Status zu implementieren. Außerdem ist die Menge der Daten, die gespeichert werden können, minimal.
  • Der Spielbildschirm wird nicht zum Ergebnisbildschirm weitergeleitet, wenn der Nutzer auf die Schaltfläche Spiel beenden tippt.

Sie können diese Probleme mit den App-Architekturkomponenten beheben, die Sie in diesem Codelab kennenlernen.

Anwendungsarchitektur

Die App-Architektur ist eine Methode zum Entwerfen der Klassen Ihrer Apps und der Beziehungen zwischen ihnen, sodass der Code organisiert ist, in bestimmten Szenarien eine gute Leistung erbringt und einfach zu verwenden ist. In dieser Reihe von vier Codelabs folgen die Verbesserungen, die Sie an der App „GuessTheWord“ vornehmen, den Richtlinien für die Android-App-Architektur. Außerdem verwenden Sie Android-Architekturkomponenten. Die Android-App-Architektur ähnelt dem MVVM-Architekturmuster (Model-View-ViewModel).

Die GuessTheWord-App folgt dem Designprinzip der Trennung von Belangen und ist in Klassen unterteilt, wobei jede Klasse einen separaten Belang abdeckt. In diesem ersten Codelab der Lektion arbeiten Sie mit einem UI-Controller, einem ViewModel und einem ViewModelFactory.

UI-Controller

Ein UI-Controller ist eine UI-basierte Klasse wie Activity oder Fragment. Ein UI-Controller sollte nur Logik enthalten, die UI- und Betriebssysteminteraktionen verarbeitet, z. B. das Anzeigen von Ansichten und das Erfassen von Nutzereingaben. Die Entscheidungslogik, z. B. die Logik, die den anzuzeigenden Text bestimmt, sollte nicht im UI-Controller enthalten sein.

Im Startcode für „GuessTheWord“ sind die UI-Controller die drei Fragmente GameFragment, ScoreFragment, und TitleFragment. Gemäß dem Designprinzip „Trennung von Belangen“ ist die GameFragment nur dafür verantwortlich, Spielelemente auf dem Bildschirm darzustellen und zu erkennen, wann der Nutzer auf die Schaltflächen tippt. Wenn der Nutzer auf eine Schaltfläche tippt, werden diese Informationen an GameViewModel übergeben.

ViewModel

Ein ViewModel enthält Daten, die in einem Fragment oder einer Aktivität angezeigt werden sollen, die mit dem ViewModel verknüpft ist. Ein ViewModel kann einfache Berechnungen und Transformationen für Daten ausführen, um die Daten für die Anzeige durch den UI-Controller vorzubereiten. In dieser Architektur trifft die ViewModel die Entscheidungen.

Die GameViewModel enthält Daten wie den Punktwert, die Liste der Wörter und das aktuelle Wort, da diese Daten auf dem Bildschirm angezeigt werden sollen. Die GameViewModel enthält auch die Geschäftslogik für einfache Berechnungen, um den aktuellen Zustand der Daten zu ermitteln.

ViewModelFactory

Ein ViewModelFactory instanziiert ViewModel-Objekte mit oder ohne Konstruktorparameter.

In späteren Codelabs erfahren Sie mehr über andere Android-Architekturkomponenten, die mit UI-Controllern und ViewModel zusammenhängen.

Die Klasse ViewModel dient zum Speichern und Verwalten der UI-bezogenen Daten. In dieser App ist jedes ViewModel mit einem Fragment verknüpft.

In dieser Aufgabe fügen Sie Ihrer App das erste ViewModel hinzu, nämlich das GameViewModel für das GameFragment. Außerdem erfahren Sie, was es bedeutet, dass die ViewModel lebenszyklusbezogen ist.

Schritt 1: GameViewModel-Klasse hinzufügen

  1. Öffnen Sie die Datei build.gradle(module:app). Fügen Sie im Block dependencies die Gradle-Abhängigkeit für ViewModel.

    hinzu. Wenn Sie die neueste Version der Bibliothek verwenden, sollte die Lösungs-App wie erwartet kompiliert werden. Wenn nicht, versuchen Sie, das Problem zu beheben, oder kehren Sie zur unten gezeigten Version zurück.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Erstellen Sie im Ordner des Pakets screens/game/ eine neue Kotlin-Klasse mit dem Namen GameViewModel.
  2. Lassen Sie die Klasse GameViewModel die abstrakte Klasse ViewModel erweitern.
  3. Damit Sie besser nachvollziehen können, wie ViewModel den Lebenszyklus berücksichtigt, fügen Sie einen init-Block mit einer log-Anweisung hinzu.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Schritt 2: onCleared() überschreiben und Logging hinzufügen

Die ViewModel wird zerstört, wenn das zugehörige Fragment getrennt oder die Aktivität beendet wird. Kurz bevor ViewModel zerstört wird, wird der onCleared()-Callback aufgerufen, um die Ressourcen zu bereinigen.

  1. Überschreiben Sie in der Klasse GameViewModel die Methode onCleared().
  2. Fügen Sie eine Log-Anweisung in onCleared() ein, um den Lebenszyklus von GameViewModel zu verfolgen.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Schritt 3: GameViewModel dem Game-Fragment zuordnen

Ein ViewModel muss einem UI-Controller zugeordnet sein. Um die beiden zu verknüpfen, erstellen Sie im UI-Controller einen Verweis auf ViewModel.

In diesem Schritt erstellen Sie einen Verweis auf GameViewModel im entsprechenden UI-Controller, also GameFragment.

  1. Fügen Sie in der Klasse GameFragment ein Feld vom Typ GameViewModel auf der obersten Ebene als Klassenvariable hinzu.
private lateinit var viewModel: GameViewModel

Schritt 4: ViewModel initialisieren

Bei Konfigurationsänderungen wie Bildschirmrotationen werden UI-Controller wie Fragmente neu erstellt. ViewModel-Instanzen bleiben jedoch erhalten. Wenn Sie die ViewModel-Instanz mit der ViewModel-Klasse erstellen, wird jedes Mal, wenn das Fragment neu erstellt wird, ein neues Objekt erstellt. Erstellen Sie die ViewModel-Instanz stattdessen mit einem ViewModelProvider.

So funktioniert ViewModelProvider:

  • ViewModelProvider gibt einen vorhandenen ViewModel zurück, falls einer vorhanden ist, oder erstellt einen neuen, falls noch keiner vorhanden ist.
  • Mit ViewModelProvider wird eine ViewModel-Instanz in Verbindung mit dem angegebenen Bereich (einer Aktivität oder einem Fragment) erstellt.
  • Das erstellte ViewModel wird so lange beibehalten, wie der Bereich aktiv ist. Wenn der Bereich beispielsweise ein Fragment ist, wird ViewModel beibehalten, bis das Fragment getrennt wird.

Initialisieren Sie ViewModel mit der Methode ViewModelProviders.of(), um ein ViewModelProvider zu erstellen:

  1. Initialisieren Sie in der Klasse GameFragment die Variable viewModel. Fügen Sie diesen Code in onCreateView() nach der Definition der Bindungsvariable ein. Verwenden Sie die Methode ViewModelProviders.of() und übergeben Sie den zugehörigen GameFragment-Kontext und die GameViewModel-Klasse.
  2. Fügen Sie vor der Initialisierung des ViewModel-Objekts eine Log-Anweisung hinzu, um den ViewModelProviders.of()-Methodenaufruf zu protokollieren.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Führen Sie die App aus. Öffnen Sie in Android Studio den Bereich Logcat und filtern Sie nach Game. Tippe auf deinem Gerät oder Emulator auf die Schaltfläche Wiedergabe. Der Spielbildschirm wird geöffnet.

    Wie im Logcat zu sehen ist, ruft die Methode onCreateView() der Klasse GameFragment die Methode ViewModelProviders.of() auf, um die Klasse GameViewModel zu erstellen. Die Logging-Anweisungen, die Sie GameFragment und GameViewModel hinzugefügt haben, werden in Logcat angezeigt.

  1. Aktivieren Sie die Einstellung „Automatisch drehen“ auf Ihrem Gerät oder Emulator und ändern Sie die Bildschirmausrichtung einige Male. Die GameFragment wird jedes Mal zerstört und neu erstellt, sodass ViewModelProviders.of() jedes Mal aufgerufen wird. GameViewModel wird jedoch nur einmal erstellt und nicht bei jedem Aufruf neu erstellt oder zerstört.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. Beenden Sie das Spiel oder verlassen Sie das Spiel-Fragment. Das GameFragment wird gelöscht. Die zugehörige GameViewModel wird ebenfalls zerstört und der Callback onCleared() wird aufgerufen.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

Die ViewModel bleibt bei Konfigurationsänderungen erhalten. Sie eignet sich daher gut für Daten, die bei Konfigurationsänderungen erhalten bleiben müssen:

  • Die Daten, die auf dem Bildschirm angezeigt werden sollen, und der Code zum Verarbeiten dieser Daten gehören in den ViewModel.
  • Die ViewModel sollte niemals Verweise auf Fragmente, Aktivitäten oder Ansichten enthalten, da Aktivitäten, Fragmente und Ansichten Konfigurationsänderungen nicht überstehen.

Zum Vergleich sehen Sie hier, wie die GameFragment-UI-Daten in der Starter-App behandelt werden, bevor und nachdem Sie ViewModel hinzugefügt haben:ViewModel

  • Vor dem Hinzufügen von ViewModel
    :Wenn sich die Konfiguration ändert, z. B. wenn der Bildschirm gedreht wird, wird das Game-Fragment zerstört und neu erstellt. Die Daten sind verloren.
  • Nachdem Sie ViewModel hinzugefügt und die UI-Daten des Spielfragments in ViewModel verschoben haben, sind alle Daten, die das Fragment zum Anzeigen benötigt, jetzt ViewModel.
    Wenn die App eine Konfigurationsänderung durchläuft, bleibt ViewModel erhalten und die Daten werden beibehalten.

In dieser Aufgabe verschieben Sie die UI-Daten der App in die Klasse GameViewModel sowie die Methoden zur Verarbeitung der Daten. So bleiben die Daten bei Konfigurationsänderungen erhalten.

Schritt 1: Datenfelder und Datenverarbeitung in das ViewModel verschieben

Verschieben Sie die folgenden Datenfelder und Methoden von GameFragment nach GameViewModel:

  1. Verschieben Sie die Datenfelder word, score und wordList. Achten Sie darauf, dass word und score nicht private sind.

    Verschieben Sie die Bindungsvariable GameFragmentBinding nicht, da sie Verweise auf die Ansichten enthält. Mit dieser Variablen wird das Layout aufgebläht, die Klick-Listener werden eingerichtet und die Daten werden auf dem Bildschirm angezeigt – alles Aufgaben des Fragments.
  2. Verschieben Sie die Methoden resetList() und nextWord(). Mit diesen Methoden wird entschieden, welches Wort auf dem Bildschirm angezeigt wird.
  3. Verschieben Sie die Methodenaufrufe für resetList() und nextWord() in der Methode onCreateView() in den init-Block von GameViewModel.

    Diese Methoden müssen sich im init-Block befinden, da die Wortliste zurückgesetzt werden sollte, wenn ViewModel erstellt wird, nicht jedes Mal, wenn das Fragment erstellt wird. Sie können die Log-Anweisung im init-Block von GameFragment löschen.

Die Klick-Handler onSkip() und onCorrect() in GameFragment enthalten Code zum Verarbeiten der Daten und Aktualisieren der Benutzeroberfläche. Der Code zum Aktualisieren der Benutzeroberfläche sollte im Fragment verbleiben, der Code zum Verarbeiten der Daten muss jedoch in die ViewModel verschoben werden.

Fügen Sie vorerst die identischen Methoden an beiden Stellen ein:

  1. Kopieren Sie die Methoden onSkip() und onCorrect() aus GameFragment in GameViewModel.
  2. Achten Sie im GameViewModel darauf, dass die Methoden onSkip() und onCorrect() nicht private sind, da Sie in Ihrem Fragment auf diese Methoden verweisen.

Hier ist der Code für die Klasse GameViewModel nach dem Refactoring:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

Hier ist der Code für die Klasse GameFragment nach dem Refactoring:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

Schritt 2: Verweise auf Klick-Handler und Datenfelder in „GameFragment“ aktualisieren

  1. Aktualisieren Sie in GameFragment die Methoden onSkip() und onCorrect(). Entfernen Sie den Code zum Aktualisieren der Punktzahl und rufen Sie stattdessen die entsprechenden onSkip()- und onCorrect()-Methoden für viewModel auf.
  2. Da Sie die Methode nextWord() in die Klasse ViewModel verschoben haben, kann das Spiel-Fragment nicht mehr darauf zugreifen.

    Ersetzen Sie in GameFragment in den Methoden onSkip() und onCorrect() den Aufruf von nextWord() durch updateScoreText() und updateWordText(). Mit diesen Methoden werden die Daten auf dem Bildschirm angezeigt.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. Aktualisieren Sie im GameFragment die Variablen score und word, damit die GameViewModel-Variablen verwendet werden, da sich diese Variablen jetzt im GameViewModel befinden.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. Entfernen Sie in GameViewModel in der Methode nextWord() die Aufrufe der Methoden updateWordText() und updateScoreText(). Diese Methoden werden jetzt über GameFragment aufgerufen.
  2. Erstellen Sie die App und achten Sie darauf, dass keine Fehler auftreten. Wenn Fehler auftreten, bereinigen Sie das Projekt und erstellen Sie es neu.
  3. Führen Sie die App aus und spielen Sie das Spiel durch. Drehen Sie das Gerät, während Sie sich auf dem Spielbildschirm befinden. Beachten Sie, dass die aktuelle Punktzahl und das aktuelle Wort nach der Änderung der Ausrichtung beibehalten werden.

Gut gemacht! Alle Daten Ihrer App werden jetzt in einem ViewModel gespeichert und bleiben daher bei Konfigurationsänderungen erhalten.

In dieser Aufgabe implementieren Sie den Click-Listener für die Schaltfläche End Game (Spiel beenden).

  1. Fügen Sie in GameFragment eine Methode namens onEndGame() hinzu. Die onEndGame()-Methode wird aufgerufen, wenn der Nutzer auf die Schaltfläche Spiel beenden tippt.
private fun onEndGame() {
   }
  1. Suchen Sie in GameFragment in der Methode onCreateView() nach dem Code, mit dem Klick-Listener für die Schaltflächen Got It (Verstanden) und Skip (Überspringen) festgelegt werden. Legen Sie direkt unter diesen beiden Zeilen einen Klick-Listener für die Schaltfläche End Game (Spiel beenden) fest. Verwenden Sie die Bindungsvariable binding. Rufen Sie im Click-Listener die Methode onEndGame() auf.
binding.endGameButton.setOnClickListener { onEndGame() }
  1. Fügen Sie in GameFragment eine Methode namens gameFinished() hinzu, um die App zum Ergebnisbildschirm zu navigieren. Übergeben Sie die Punktzahl als Argument mit Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. Rufen Sie in der Methode onEndGame() die Methode gameFinished() auf.
private fun onEndGame() {
   gameFinished()
}
  1. Führen Sie die App aus, spielen Sie das Spiel und wechseln Sie zwischen einigen Wörtern. Tippen Sie auf die Schaltfläche Spiel beenden . Die App wechselt zum Bildschirm mit dem Ergebnis, aber das Endergebnis wird nicht angezeigt. Das beheben Sie in der nächsten Aufgabe.

Wenn der Nutzer das Spiel beendet, wird in ScoreFragment keine Punktzahl angezeigt. Sie möchten, dass ein ViewModel den von ScoreFragment angezeigten Wert enthält. Sie übergeben den Punktwert während der ViewModel-Initialisierung mit dem Factory-Method-Muster.

Das Factory-Method-Muster ist ein Muster für die Erstellung, bei dem Factory-Methoden zum Erstellen von Objekten verwendet werden. Eine Factory-Methode ist eine Methode, die eine Instanz derselben Klasse zurückgibt.

In dieser Aufgabe erstellen Sie ein ViewModel mit einem parametrisierten Konstruktor für das Score-Fragment und einer Factory-Methode zum Instanziieren des ViewModel.

  1. Erstellen Sie unter dem Paket score eine neue Kotlin-Klasse mit dem Namen ScoreViewModel. Diese Klasse ist die ViewModel für das Score-Fragment.
  2. Erweitern Sie die Klasse ScoreViewModel von ViewModel.. Fügen Sie einen Konstruktorparameter für die endgültige Punktzahl hinzu. Fügen Sie einen init-Block mit einer Log-Anweisung hinzu.
  3. Fügen Sie in der Klasse ScoreViewModel eine Variable namens score hinzu, um die endgültige Punktzahl zu speichern.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. Erstellen Sie unter dem Paket score eine weitere Kotlin-Klasse namens ScoreViewModelFactory. Diese Klasse ist für die Instanziierung des ScoreViewModel-Objekts verantwortlich.
  2. Erweitern Sie die Klasse ScoreViewModelFactory aus ViewModelProvider.Factory. Fügen Sie einen Konstruktorparameter für die endgültige Punktzahl hinzu.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. In ScoreViewModelFactory wird in Android Studio ein Fehler zu einem nicht implementierten abstrakten Member angezeigt. Überschreiben Sie die Methode create(), um den Fehler zu beheben. Geben Sie in der Methode create() das neu erstellte ScoreViewModel-Objekt zurück.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. Erstellen Sie in ScoreFragment Klassenvariablen für ScoreViewModel und ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. Initialisieren Sie in ScoreFragment innerhalb von onCreateView() nach der Initialisierung der Variablen binding die viewModelFactory. Verwenden Sie das ScoreViewModelFactory. Übergeben Sie den endgültigen Wert aus dem Argument-Bundle als Konstruktorparameter an ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Initialisieren Sie in onCreateView( nach der Initialisierung von viewModelFactory das Objekt viewModel. Rufen Sie die Methode ViewModelProviders.of() auf und übergeben Sie den zugehörigen Kontext des Score-Fragments und viewModelFactory. Dadurch wird das ScoreViewModel-Objekt mit der in der Klasse viewModelFactory definierten Factory-Methode erstellt..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. Legen Sie in der Methode onCreateView() nach der Initialisierung von viewModel den Text der Ansicht scoreText auf die in ScoreViewModel definierte Endpunktzahl fest.
binding.scoreText.text = viewModel.score.toString()
  1. Führen Sie die App aus und spielen Sie das Spiel. Gehen Sie einige oder alle Wörter durch und tippen Sie auf Spiel beenden. Im Score-Fragment wird jetzt das Endergebnis angezeigt.

  1. Optional: Prüfen Sie die ScoreViewModel-Logs im Logcat, indem Sie nach ScoreViewModel filtern. Der Punktwert sollte angezeigt werden.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

In dieser Aufgabe haben Sie ScoreFragment implementiert, um ViewModel zu verwenden. Außerdem haben Sie gelernt, wie Sie mit der ViewModelFactory-Schnittstelle einen parametrisierten Konstruktor für ein ViewModel erstellen.

Glückwunsch! Sie haben die Architektur Ihrer App so geändert, dass eine der Android-Architekturkomponenten verwendet wird: ViewModel. Sie haben das Problem mit dem Lebenszyklus der App behoben und die Daten des Spiels bleiben jetzt bei Konfigurationsänderungen erhalten. Außerdem haben Sie gelernt, wie Sie einen parametrisierten Konstruktor zum Erstellen eines ViewModel mithilfe der ViewModelFactory-Schnittstelle erstellen.

Android Studio-Projekt: GuessTheWord

  • In den Richtlinien für die Android-App-Architektur wird empfohlen, Klassen mit unterschiedlichen Verantwortlichkeiten zu trennen.
  • Ein UI-Controller ist eine UI-basierte Klasse wie Activity oder Fragment. UI-Controller sollten nur Logik enthalten, die UI- und Betriebssysteminteraktionen verarbeitet. Sie sollten keine Daten enthalten, die in der Benutzeroberfläche angezeigt werden sollen. Fügen Sie diese Daten in eine ViewModel ein.
  • In der Klasse ViewModel werden UI-bezogene Daten gespeichert und verwaltet. Mit der Klasse ViewModel können Daten Konfigurationsänderungen wie Bildschirmrotationen überstehen.
  • ViewModel ist eine der empfohlenen Android-Architekturkomponenten.
  • ViewModelProvider.Factory ist eine Schnittstelle, mit der Sie ein ViewModel-Objekt erstellen können.

In der Tabelle unten werden UI-Controller mit den ViewModel-Instanzen verglichen, die Daten für sie enthalten:

UI-Controller

ViewModel

Ein Beispiel für einen UI-Controller ist der ScoreFragment, den Sie in diesem Codelab erstellt haben.

Ein Beispiel für ein ViewModel ist das ScoreViewModel, das Sie in diesem Codelab erstellt haben.

Enthält keine Daten, die in der Benutzeroberfläche angezeigt werden können.

Enthält Daten, die vom UI-Controller in der Benutzeroberfläche angezeigt werden.

Enthält Code zum Anzeigen von Daten und Nutzerereigniscode wie Klick-Listener.

Enthält Code für die Datenverarbeitung.

Bei jeder Konfigurationsänderung wird sie zerstört und neu erstellt.

Wird nur zerstört, wenn der zugehörige UI-Controller dauerhaft entfernt wird – bei einer Aktivität, wenn die Aktivität beendet wird, oder bei einem Fragment, wenn das Fragment getrennt wird.

Enthält Ansichten.

Darf niemals Verweise auf Aktivitäten, Fragmente oder Ansichten enthalten, da diese Konfigurationsänderungen nicht überstehen, ViewModel jedoch schon.

Enthält einen Verweis auf die zugehörige ViewModel.

Enthält keinen Verweis auf den zugehörigen UI-Controller.

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

In welcher Klasse sollten Sie App-Daten speichern, um Datenverlust bei einer Änderung der Gerätekonfiguration zu vermeiden?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Frage 2

Eine ViewModel darf niemals Verweise auf Fragmente, Aktivitäten oder Ansichten enthalten. Richtig oder falsch?

  • Richtig
  • Falsch

Frage 3

Wann wird ein ViewModel zerstört?

  • Wenn der zugehörige UI-Controller bei einer Änderung der Geräteausrichtung zerstört und neu erstellt wird.
  • Bei einer Änderung der Ausrichtung.
  • Wenn der zugehörige UI-Controller beendet (bei einer Aktivität) oder getrennt (bei einem Fragment) wurde.
  • Wenn der Nutzer die Zurück-Schaltfläche drückt.

Frage 4

Wozu dient die ViewModelFactory-Schnittstelle?

  • Instanziieren eines ViewModel-Objekts.
  • Daten bei Änderungen der Ausrichtung beibehalten
  • Die auf dem Bildschirm angezeigten Daten werden aktualisiert.
  • Benachrichtigungen erhalten, wenn die App-Daten geändert werden.

5.2: LiveData und LiveData-Beobachter

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