Android Kotlin Fundamentals 05.1: ViewModel und ViewModelFactory

Dieses Codelab ist Teil des Android Kotlin Fundamentals-Kurss. Sie profitieren von diesem Kurs, wenn Sie nacheinander die Codelabs durcharbeiten. Alle Kurs-Codelabs finden Sie auf der Landingpage für Kotlin-Grundlagen für Android-Entwickler.

Titelbildschirm

Spiele-Bildschirm

Index erstellen

Einführung

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

  • Mit der Klasse ViewModel speichern und verwalten Sie UI-bezogene Daten für den Lebenszyklus. Mit der ViewModel-Klasse werden Daten auch bei Änderungen der Gerätekonfiguration übernommen, z. B. Bildschirmdrehungen und Änderungen der Tastaturverfügbarkeit.
  • Sie verwenden die Klasse ViewModelFactory, um das Objekt ViewModel zu instanziieren und zurückzugeben, das Konfigurationsänderungen übersteht.

Was Sie bereits wissen sollten

  • Grundlegende Android-Apps in Kotlin erstellen.
  • Mit dem Navigationsdiagramm die Navigation in Ihrer App implementieren
  • Code hinzufügen, um zwischen den Zielen Ihrer App zu wechseln und Daten zwischen Navigationszielen zu übergeben
  • Funktionsweise der Aktivitäts- und Fragmentlebenszyklen
  • Anleitung zum Hinzufügen von Logging-Informationen zu einer App und zum Lesen von Logs mit Logcat in Android Studio

Lerninhalte

  • Wie Sie die empfohlene Android-App-Architektur verwenden
  • Wie werden die Klassen Lifecycle, ViewModel und ViewModelFactory in Ihrer App verwendet?
  • UI-Daten aufgrund von Änderungen an der Gerätekonfiguration beibehalten.
  • Was das Factory-Design ist und wie es verwendet wird
  • So lässt sich ein ViewModel-Objekt über die Schnittstelle ViewModelProvider.Factory erstellen.

Aufgaben

  • Füge der App ein ViewModel hinzu, um App-Daten zu speichern, damit die Daten nach einer Konfigurationsänderung übernommen werden.
  • Sie können ViewModelFactory und das Designmuster für Factory-Methoden verwenden, um ein ViewModel-Objekt mit Konstruktorparametern zu instanziieren.

In Modul 5 der Lektion 5 entwickeln Sie die GuessTheWord-App, die mit dem Startcode beginnt. GuessTheWord ist ein Spiel für zwei Spieler, das im Spiel Charades entwickelt und in dem Spieler die höchstmögliche Punktzahl erzielen.

Der erste Spieler betrachtet die Wörter in der App und agiert nacheinander. Dabei wird dem zweiten Spieler das Wort nicht angezeigt. Der zweite Spieler versucht, das Wort zu erraten.

Zum Spielen öffnet der erste Spieler die App auf dem Gerät und sieht ein Wort, z. B. „Gitarre“, unten.

Der erste Spieler führt das Wort aus und achte darauf, nicht das Wort selbst zu sagen.

  • Wenn der zweite Spieler das Wort richtig errät, klickt der erste Spieler auf OK. 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 der Wörter um eins verringert und zum nächsten Wort springen.
  • Klicken Sie auf die Schaltfläche Spiel beenden, um das Spiel zu beenden. Diese Funktion befindet sich im Starter-Code für das erste Codelab der Serie.

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

Schritt 1: Erste Schritte

  1. Laden Sie den Startcode von GuessTheWord 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. Tippen Sie auf die Tasten. Beachten Sie, dass mit der Schaltfläche Überspringen das nächste Wort angezeigt und die Punktzahl um eins verringert wird. Mit der Schaltfläche OK wird das nächste Wort angezeigt und die Punktzahl um eins erhöht. Die Schaltfläche Spiel beenden ist nicht implementiert, also passiert nichts, wenn du darauf tippst.

Schritt 2: Code Schritt-für-Schritt-Anleitung ausführen

  1. Sieh dir den Code in Android Studio an, um eine Vorstellung davon zu bekommen, wie die App funktioniert.
  2. Sehen Sie sich die unten beschriebenen Dateien an, die besonders wichtig sind.

Hauptaktivität.kt

Diese Datei enthält nur den standardmäßigen, von der Vorlage generierten Code.

res/layout/main_activity.xml

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

UI-Fragmente

Der Starter-Code hat drei Fragmente in drei verschiedenen Paketen unter dem com.example.android.guesstheword.screens-Paket:

  • title/TitleFragment für den Titelbildschirm
  • game/GameFragment für Spielebildschirm
  • score/ScoreFragment für die Punktezahl

Bildschirme/Titel/Titelfragment.kt

Das Titelfragment ist der erste Bildschirm, der angezeigt wird, wenn die App gestartet wird. Ein Klick-Handler ist auf die Schaltfläche Wiedergabe festgelegt, über die Sie zum Spielbildschirm gelangen können.

Bildschirme/Spiele/GameFragment.kt

Das ist das Fragment, in dem die meisten Aktionen des Spiels stattfinden:

  • Variablen werden für das aktuelle Wort und die aktuelle Punktzahl definiert.
  • Die wordList, die in der resetList()-Methode definiert ist, ist eine Beispielliste der Wörter, die im Spiel verwendet werden sollen.
  • Die Methode onSkip() ist der Klick-Handler für die Schaltfläche Überspringen. Der Wert verringert sich um 1, anschließend wird mit der nextWord()-Methode das nächste Wort angezeigt.
  • Die Methode onCorrect() ist der Klick-Handler für die Schaltfläche OK. Diese Methode wird ähnlich wie die Methode onSkip() implementiert. Der einzige Unterschied besteht darin, dass diese Methode mit 1 bewertet wird und nicht subtrahiert wird.

Screens/Score/ScoreFragment.kt

ScoreFragment ist der letzte Bildschirm im Spiel und das Spielergebnis wird angezeigt. In diesem Codelab fügen Sie die Implementierung hinzu, damit dieser Bildschirm angezeigt wird und die endgültige Punktzahl angezeigt wird.

res/navigation/main_navigation.xml

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

  • Über das Titelfragment kann der Nutzer das Spielfragment aufrufen.
  • Der Nutzer kann über das Spielfragment zum Wertfragment gelangen.
  • Über das Punktzahlfragment kann der Nutzer zurück zum Spielfragment wechseln.

Bei dieser Aufgabe finden Sie Probleme mit der Start-App von GuessTheWord.

  1. Führen Sie den Startcode aus und spielen Sie das Spiel mit einigen Wörtern. Tippen Sie nach jedem Wort auf Überspringen oder Ok.
  2. Auf dem Spielbildschirm sind jetzt ein Wort und der aktuelle Spielstand zu sehen. Bildschirmausrichtung ändern, indem das Gerät oder der Emulator gedreht wird Beachten Sie, dass die aktuelle Punktzahl verloren geht.
  3. Führen Sie das Spiel noch ein paar Wörter aus. Wenn auf dem Bildschirm ein Spielergebnis angezeigt wird, schließen Sie die App und öffnen Sie sie wieder. Das Spiel beginnt von vorn, da der App-Status nicht gespeichert wird.
  4. Spielen Sie das Spiel mit einigen Wörtern und tippen Sie dann auf die Schaltfläche Spiel beenden. Beachte, dass nichts passiert.

Probleme mit der App:

  • Der Startstatus der App wird bei Konfigurationsänderungen nicht gespeichert und wiederhergestellt, z. B. wenn sich die Geräteausrichtung ändert oder die App heruntergefahren und neu gestartet wird.
    Sie können das Problem mit dem onSaveInstanceState()-Callback beheben. Wenn Sie die Methode onSaveInstanceState() verwenden, müssen Sie jedoch zusätzlichen Code schreiben, um den Zustand in einem Paket zu speichern, und dann die Logik zum Abrufen dieses Status implementieren. Außerdem ist die Menge der gespeicherten Daten minimal.
  • Der Spielbildschirm öffnet den Spielstand nicht, wenn der Nutzer auf die Schaltfläche Spiel beenden tippt.

Sie können diese Probleme mithilfe der Komponenten der App-Architektur in diesem Codelab beheben.

Anwendungsarchitektur

Mithilfe der App-Architektur können Sie Ihre Apps und die Beziehungen zu ihnen so gestalten, dass der Code organisiert ist, in bestimmten Szenarien gut funktioniert und einfach funktioniert. In diesem Satz mit vier Codelabs entsprechen die Verbesserungen der GuessTheWord-App den Richtlinien für die Android-App-Architektur und Sie verwenden die Komponenten der Android-Architektur. Die Android-App-Architektur ähnelt dem MVVM-Architekturmuster (model-view-viewmodel).

Die GuessTheWord-App folgt dem Grundsatz zur Trennung von Bedenken und ist in Klassen unterteilt, wobei jede Klasse einem anderen Thema entspricht. In diesem ersten Codelab der Lektion bearbeiten Sie einen UI-Controller, einen ViewModel und einen ViewModelFactory.

UI-Controller

Ein UI-Controller ist eine UI-basierte Klasse wie Activity oder Fragment. Ein UI-Controller sollte nur eine Logik enthalten, die die UI und Betriebssysteminteraktionen verarbeitet, wie z. B. Aufrufe und Erfassung der Nutzereingaben. Setzen Sie keine Entscheidungslogik, wie z. B. eine Logik, durch die der anzuzeigende Text bestimmt wird, in den UI-Controller.

Im GuessTheWord-Startcode sind die UI-Controller die drei Fragmente: GameFragment, ScoreFragment, und TitleFragment. Gemäß dem Konzept der Trennung von Bedenken ist das GameFragment nur dafür verantwortlich, Spielelemente auf dem Bildschirm anzuzeigen und zu wissen, wann der Nutzer auf die Schaltflächen tippt, und nichts mehr. Wenn der Nutzer auf eine Schaltfläche tippt, werden diese Informationen an GameViewModel gesendet.

ViewModel

Ein ViewModel enthält Daten, die in einem Fragment oder einer Aktivität angezeigt werden sollen, die mit der ViewModel verknüpft sind. Ein ViewModel kann einfache Berechnungen und Transformationen für Daten vornehmen, um die Daten für die Anzeige durch den UI-Controller vorzubereiten. In dieser Architektur führt der ViewModel die Entscheidungsfindung aus.

Der GameViewModel enthält Daten wie den Wert (Punktzahl), die Wortliste und das aktuelle Wort, da diese Daten auf dem Bildschirm angezeigt werden. Der GameViewModel enthält auch die Geschäftslogik für einfache Berechnungen, um den aktuellen Zustand der Daten zu ermitteln.

Modellmodell ansehen

Ein ViewModelFactory instanziiert ViewModel-Objekte mit oder ohne Konstruktorparameter.

In späteren Codelabs erhalten Sie weitere Informationen zu Komponenten der Android-Architektur, die mit UI-Controllern und ViewModel zusammenhängen.

Die Klasse ViewModel wurde entwickelt, um die UI-bezogenen Daten zu speichern und zu verwalten. In dieser App ist jede ViewModel einem Fragment zugeordnet.

In dieser Aufgabe fügen Sie Ihre erste ViewModel zu Ihrer App hinzu, die GameViewModel für die GameFragment. Außerdem erfahren Sie, was das ViewModel bedeutet, dass der Lebenszyklus berücksichtigt wird.

Schritt 1: GameViewModel-Klasse hinzufügen

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

    Wenn du die aktuelle Version der Bibliothek verwendest, sollte die Lösungs-App wie erwartet kompiliert werden. Ist dies nicht der Fall, versuchen Sie, das Problem zu beheben, oder kehren Sie zur unten angezeigten Version zurück.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Erstellen Sie im Ordner screens/game/ des Pakets eine neue Kotlin-Klasse mit dem Namen GameViewModel.
  2. GameViewModel muss 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

ViewModel wird gelöscht, wenn das zugehörige Fragment getrennt oder die Aktivität beendet wurde. Direkt vor dem Löschen von ViewModel wird der onCleared()-Callback aufgerufen, um die Ressourcen zu bereinigen.

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

Schritt 3: GameViewModel dem Spielfragment zuordnen

Ein ViewModel muss mit einem UI-Controller verknüpft werden. Zum Verknüpfen der beiden erstellen Sie einen Verweis auf die ViewModel im UI-Controller.

In diesem Schritt erstellen Sie einen Verweis auf die GameViewModel im zugehörigen UI-Controller: GameFragment.

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

Schritt 4: ViewModel initialisieren

Während Konfigurationsänderungen wie Bildschirmdrehungen werden UI-Controller wie Fragmente neu erstellt. ViewModel-Instanzen sind jedoch erhalten. Wenn du die ViewModel-Instanz mit der Klasse ViewModel erstellst, wird bei jeder Neuerstellung des Fragments ein neues Objekt erstellt. Erstellen Sie stattdessen die ViewModel-Instanz mit einem ViewModelProvider.

So funktioniert ViewModelProvider:

  • ViewModelProvider gibt einen vorhandenen ViewModel zurück, wenn einer vorhanden ist, oder erstellt einen neuen, wenn dieser nicht bereits vorhanden ist.
  • ViewModelProvider erstellt eine ViewModel-Instanz in Verbindung mit dem angegebenen Bereich (einer Aktivität oder einem Fragment).
  • Die erstellte ViewModel wird aufbewahrt, solange der Bereich aktiv ist. Wenn der Bereich beispielsweise ein Fragment ist, wird die ViewModel beibehalten, bis das Fragment getrennt wird.

Initialisiere ViewModel mit der ViewModelProviders.of()-Methode, um eine ViewModelProvider zu erstellen:

  1. Initialisiere in der Klasse GameFragment die Variable viewModel. Fügen Sie diesen Code nach der Definition der Bindungsvariablen in onCreateView() ein. Verwende die Methode ViewModelProviders.of() und übergib den zugehörigen GameFragment-Kontext und die GameViewModel-Klasse.
  2. Fügen Sie über der Initialisierung des ViewModel-Objekts eine Log-Anweisung hinzu, um den Methodenaufruf ViewModelProviders.of() 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. Tippen Sie auf Ihrem Gerät oder Emulator auf die Schaltfläche Wiedergabe. Der Spielbildschirm wird geöffnet.

    Wie im Logcat gezeigt, ruft die onCreateView()-Methode von GameFragment die ViewModelProviders.of()-Methode auf, um die GameViewModel zu erstellen. Die Logging-Anweisungen, die du GameFragment und GameViewModel hinzugefügt hast, werden im Logcat angezeigt.

  1. Aktivieren Sie die Einstellung für das automatische Drehen auf Ihrem Gerät oder in Ihrem Emulator und ändern Sie die Bildschirmausrichtung einige Male. Die GameFragment wird jedes Mal gelöscht und neu erstellt, sodass ViewModelProviders.of() jedes Mal aufgerufen wird. Die GameViewModel wird aber nur einmal erstellt und nicht für jeden Aufruf neu erstellt oder gelöscht.
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 Spielfragment. Das GameFragment wurde gelöscht. Das zugehörige GameViewModel wird ebenfalls gelöscht 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!

ViewModel bleibt auch bei Konfigurationsänderungen bestehen. Sie eignet sich daher gut für Daten, bei denen Konfigurationsänderungen übernommen werden müssen:

  • Füge Daten hinzu, die auf dem Bildschirm angezeigt werden sollen, und Code, um diese Daten in der ViewModel zu verarbeiten.
  • ViewModel sollte niemals Verweise auf Fragmente, Aktivitäten oder Datenansichten enthalten, da Aktivitäten, Fragmente und Datenansichten keine Änderungen an der Konfiguration überstehen.

So werden Daten der Benutzeroberfläche GameFragment in der Starter-App vor dem Hinzufügen von ViewModel und nach dem Hinzufügen von ViewModel verarbeitet:

  • Bevor du ViewModel hinzufügst:
    Wenn die App eine Konfigurationsänderung durchläuft, z. B. eine Bildschirmdrehung, wird das Spielfragment gelöscht und neu erstellt. Die Daten gehen verloren.
  • Nachdem du ViewModel hinzugefügt und die UI-Daten des Spielfragments in ViewModel verschoben hast:
    Alle Daten, die das Fragment anzeigen soll, sind jetzt die ViewModel. Wenn die Anwendung eine Konfigurationsänderung durchläuft, bleibt ViewModel bestehen und die Daten bleiben erhalten.

Bei dieser Aufgabe verschieben Sie die UI-Daten der App in die GameViewModel-Klasse sowie die Methoden zur Datenverarbeitung. So bleiben die Daten während der Konfigurationsänderungen erhalten.

Schritt 1: Datenfelder und Datenverarbeitung in „ViewModel“ verschieben

Verschieben Sie die folgenden Datenfelder und Methoden aus GameFragment in GameViewModel:

  1. Verschieben Sie die Datenfelder word, score und wordList. word und score dürfen nicht private sein.

    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 eingerichtet und die Daten auf dem Bildschirm angezeigt – die Verantwortlichkeiten des Fragments.
  2. Verschieben Sie die Methoden resetList() und nextWord(). Mit diesen Methoden wird festgelegt, welches Wort auf dem Bildschirm angezeigt wird.
  3. Verschieben Sie innerhalb der onCreateView()-Methode die Aufrufe der Methode nach resetList() und nextWord() in den init-Block der GameViewModel.

    Diese Methoden müssen sich im init-Block befinden, da du die Wortliste bei der Erstellung von ViewModel zurücksetzen solltest, und nicht bei jeder Erstellung des Fragments. Sie können die Loganweisung im init-Block von GameFragment löschen.

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

Fügen Sie an beiden Stellen die gleichen Methoden ein:

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

Hier ist der Code für die GameViewModel-Klasse nach Refaktorierung:

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 Refaktorierung:

/**
* 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. Aktualisiere in GameFragment die Methoden onSkip() und onCorrect(). Entfernen Sie den Code, um den Wert zu aktualisieren, und rufen Sie stattdessen die entsprechenden Methoden onSkip() und onCorrect() in viewModel auf.
  2. Weil du die nextWord()-Methode in die ViewModel verschoben hast, kann das Spielfragment nicht mehr darauf zugreifen.

    Ersetzen Sie 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 in den GameFragment-Variablen score und word, damit die Variablen GameViewModel verwendet werden, weil diese Variablen nun in der GameViewModel sind.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. Entfernen Sie in der Methode GameViewModel innerhalb der Methode nextWord() die Aufrufe für die Methoden updateWordText() und updateScoreText(). Diese Methoden werden jetzt aus GameFragment aufgerufen.
  2. Erstellen Sie die App und stellen Sie sicher, dass sie keine Fehler enthält. Bei Fehlern können Sie das Projekt bereinigen und neu erstellen.
  3. Führen Sie die App aus und spielen Sie das Spiel mit einigen Wörtern. Drehen Sie das Gerät, während Sie sich auf dem Spielbildschirm befinden. Beachten Sie, dass der aktuelle Wert und das aktuelle Wort nach der Änderung der Ausrichtung beibehalten werden.

Gut gemacht! Alle Ihre App-Daten werden jetzt in einem ViewModel gespeichert, sodass sie bei Konfigurationsänderungen erhalten bleiben.

In dieser Aufgabe implementieren Sie den Klick-Listener für die Schaltfläche Spiel beenden.

  1. Füge in GameFragment eine Methode namens onEndGame() hinzu. Die Methode onEndGame() wird aufgerufen, wenn der Nutzer auf die Schaltfläche Spiel beenden tippt.
private fun onEndGame() {
   }
  1. Suchen Sie in der onCreateView()-Methode zu GameFragment den Code, der die Klick-Listener für die Schaltflächen Ok und Überspringen festlegt. Legen Sie direkt unter diesen beiden Zeilen einen Klick-Listener für die Schaltfläche Spiel beenden fest. Verwenden Sie die Bindungsvariable binding. Rufen Sie im Klick-Listener die Methode onEndGame() auf.
binding.endGameButton.setOnClickListener { onEndGame() }
  1. Fügen Sie in GameFragment eine Methode namens gameFinished() hinzu, um zur App zu gelangen und zum Punktzahlbildschirm zu gelangen. Übergeben Sie den Wert mit Safe Args als Argument.
/**
* 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. Rufe in der Methode onEndGame() die Methode gameFinished() auf.
private fun onEndGame() {
   gameFinished()
}
  1. Führe die App aus, spiele das Spiel und durchlaufe einige Wörter. Tippen Sie auf die Schaltfläche Spiel beenden. Beachten Sie, dass die App den Punktestand anzeigt, allerdings wird der endgültige Wert nicht angezeigt. Dieses Problem beheben Sie in der nächsten Aufgabe.

Wenn der Nutzer das Spiel beendet, wird der Optimierungsfaktor durch ScoreFragment nicht angezeigt. Sie möchten, dass ein ViewModel die Punktzahl auf ScoreFragment festhält. Sie übergeben den Punktzahl-Wert während der ViewModel-Initialisierung mit dem Factory-Muster-Muster.

Das Factory-Muster ist ein Designdesign, bei dem Objekte mithilfe von Factory-Methoden erstellt werden. Eine Factory-Methode ist eine Methode, die eine Instanz derselben Klasse zurückgibt.

In dieser Aufgabe erstellen Sie eine ViewModel mit einem parametrisierten Konstruktor für das Punktzahlfragment und einer Factory-Methode zur Instanziierung 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 Punktzahlfragment.
  2. Erweitern Sie die ScoreViewModel-Klasse von ViewModel.. Fügen Sie einen Konstruktorparameter für die endgültige Punktzahl hinzu. Fügen Sie einen init-Block mit einer Loganweisung hinzu.
  3. Fügen Sie in der Klasse ScoreViewModel eine Variable namens score hinzu, um die Gesamtpunktzahl 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 mit dem Namen ScoreViewModelFactory. Diese Klasse ist für die Instanziierung des ScoreViewModel-Objekts verantwortlich.
  2. Erweitern Sie die ScoreViewModelFactory-Klasse von ViewModelProvider.Factory. Fügen Sie einen Konstruktorparameter für die Gesamtpunktzahl hinzu.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. In ScoreViewModelFactory wird in Android Studio ein Fehler in Bezug auf ein nicht implementiertes abstraktes Mitglied angezeigt. Überschreiben Sie die create()-Methode, um den Fehler zu beheben. Gib in der create()-Methode 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 die Klassenvariablen für ScoreViewModel und ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. Initialisiere in ScoreFragment innerhalb von onCreateView() nach der Initialisierung der Variable binding den viewModelFactory. Verwende ScoreViewModelFactory. Übergeben Sie die endgültige Punktzahl aus dem Argumentpaket als Konstruktorparameter an ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Initialisiere nach onCreateView( viewModelFactory das Objekt viewModel. Rufen Sie die Methode ViewModelProviders.of() auf, übergeben Sie den zugehörigen Bewertungsfragment-Kontext und viewModelFactory. Dadurch wird das Objekt ScoreViewModel mit der in der viewModelFactory-Klasse definierten Factory-Methode erstellt..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. Legen Sie nach der Initialisierung von viewModel in onCreateView() den Text der Ansicht scoreText auf den endgültigen Wert für ScoreViewModel fest.
binding.scoreText.text = viewModel.score.toString()
  1. Führe deine App aus und spiel das Game. Gehen Sie einzelne Wörter oder alle Wörter durch und tippen Sie auf Spiel beenden. Das Bewertungsfragment zeigt jetzt die endgültige Punktzahl an.

  1. Optional: Prüfen Sie die ScoreViewModel-Protokolle im Logcat, indem Sie nach ScoreViewModel filtern. Der Wert 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 für die Verwendung von ViewModel implementiert. Außerdem haben Sie gelernt, wie Sie über die Schnittstelle ViewModelFactory einen parametrisierten Konstruktor für einen ViewModel erstellen.

Glückwunsch! Sie haben die Architektur Ihrer App so geändert, dass eine der Android-Architekturkomponenten (ViewModel) verwendet wird. Du hast das Lebenszyklusproblem in der App behoben. Jetzt sind die Konfigurationsdaten des Spiels erhalten. Außerdem haben Sie gelernt, wie Sie mit der Schnittstelle ViewModelFactory einen parametrisierten Konstruktor zum Erstellen eines ViewModel erstellen.

Android Studio-Projekt: GuessTheWord

  • Die Android-Richtlinien für die App-Architektur empfehlen, 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 UI angezeigt werden sollen. Füge diese Daten in ein ViewModel ein.
  • In der Klasse ViewModel werden Daten im Zusammenhang mit der Benutzeroberfläche gespeichert und verwaltet. Mit der ViewModel-Klasse können Daten auch Konfigurationsänderungen wie Bildschirmdrehen überleben.
  • ViewModel ist eine der empfohlenen Android-Architekturkomponenten.
  • ViewModelProvider.Factory ist eine Schnittstelle, mit der Sie ein ViewModel-Objekt erstellen können.

In der folgenden Tabelle finden Sie einen Vergleich der UI-Controller mit den ViewModel-Instanzen, die Daten für sie enthalten:

UI-Controller

Modell aufrufen

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

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

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

Enthält Daten, die vom UI-Controller in der UI 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 wurde sie gelöscht und neu erstellt.

Wird nur gelöscht, wenn der zugehörige UI-Controller dauerhaft verschwindet – für eine Aktivität, wenn die Aktivität abgeschlossen ist, oder für ein Fragment, wenn das Fragment getrennt ist.

Enthält Aufrufe

Darf keine Verweise auf Aktivitäten, Fragmente oder Ansichten enthalten, da sie Konfigurationsänderungen nicht überleben, aber ViewModel.

Enthält einen Verweis auf die verknüpfte ViewModel.

Enthält keine Referenz auf den verknüpften UI-Controller.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Sonstiges:

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler oder Studenten aufgeführt, die an diesem von einem Kursleiter geleiteten Codelab arbeiten. Die Lehrkraft kann Folgendes tun:

  • Bei Bedarf können Sie die entsprechenden Aufgaben zuweisen.
  • Schülern mitteilen, wie sie Aufgaben für die Aufgabe abgeben
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge so oft oder so oft verwenden, wie sie möchten. anderen Aufgaben können sie nach Belieben zugewiesen werden.

Wenn Sie alleine an diesem Codelab arbeiten, können Sie Ihr Wissen mit diesen Hausaufgaben testen.

Diese Fragen beantworten

Frage 1

In welcher Klasse sollten Sie App-Daten speichern, um Datenverluste während der Änderung der Gerätekonfiguration zu vermeiden?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Frage 2

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

  • Richtig
  • Falsch

Frage 3

Wann wird ViewModel gelöscht?

  • Wenn der zugehörige UI-Controller während einer Änderung der Geräteausrichtung gelöscht und neu erstellt wird.
  • Änderungen der Ausrichtung
  • Wenn der zugehörige UI-Controller fertig ist (wenn es eine Aktivität ist) oder getrennt (wenn er ein Fragment ist).
  • Wenn der Nutzer die Schaltfläche „Zurück“ drückt

Frage 4

Welchen Zweck hat die ViewModelFactory-Schnittstelle?

  • Instanziiert ein ViewModel-Objekt.
  • Daten bei Änderungen der Ausrichtung beibehalten.
  • Die auf dem Bildschirm angezeigten Daten werden aktualisiert.
  • Ich erhalte Benachrichtigungen, wenn die App-Daten geändert werden.

Beginnen Sie mit der nächsten Lektion: 5.2: LiveData- und LiveData-Betrachter

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