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 KlasseViewModel
können Daten Änderungen der Gerätekonfiguration wie Bildschirmdrehungen und Änderungen der Tastaturverfügbarkeit überdauern. - Mit der Klasse
ViewModelFactory
instanziieren und geben Sie dasViewModel
-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
- So verwenden Sie die empfohlene App-Architektur für Android.
- So verwenden Sie die Klassen
Lifecycle
,ViewModel
undViewModelFactory
in Ihrer App. - UI-Daten bei Änderungen der Gerätekonfiguration beibehalten
- Was das Fabrikmethoden-Entwurfsmuster ist und wie es verwendet wird.
- So erstellen Sie ein
ViewModel
-Objekt mit der SchnittstelleViewModelProvider.Factory
.
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 einViewModel
-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
- Laden Sie den GuessTheWord-Startcode herunter und öffnen Sie das Projekt in Android Studio.
- Führen Sie die App auf einem Android-Gerät oder in einem Emulator aus.
- 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
- Sehen Sie sich den Code in Android Studio an, um ein Gefühl dafür zu bekommen, wie die App funktioniert.
- 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 Titelbildschirmgame/GameFragment
für den Spielbildschirmscore/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 MethoderesetList()
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 MethodenextWord()
angezeigt. - Die Methode
onCorrect()
ist der Klick-Handler für die Schaltfläche Verstanden. Diese Methode wird ähnlich wie die MethodeonSkip()
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“.
- Führen Sie den Startercode aus und spielen Sie das Spiel einige Wörter lang. Tippen Sie nach jedem Wort auf Überspringen oder Verstanden.
- 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.
- 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.
- 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 demonSaveInstanceState()
-Callback beheben. Wenn Sie dieonSaveInstanceState()
-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
- Öffnen Sie die Datei
build.gradle(module:app)
. Fügen Sie im Blockdependencies
die Gradle-Abhängigkeit fürViewModel
.
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'
- Erstellen Sie im Ordner des Pakets
screens/game/
eine neue Kotlin-Klasse mit dem NamenGameViewModel
. - Lassen Sie die Klasse
GameViewModel
die abstrakte KlasseViewModel
erweitern. - Damit Sie besser nachvollziehen können, wie
ViewModel
den Lebenszyklus berücksichtigt, fügen Sie eineninit
-Block mit einerlog
-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.
- Überschreiben Sie in der Klasse
GameViewModel
die MethodeonCleared()
. - Fügen Sie eine Log-Anweisung in
onCleared()
ein, um den Lebenszyklus vonGameViewModel
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
.
- Fügen Sie in der Klasse
GameFragment
ein Feld vom TypGameViewModel
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 vorhandenenViewModel
zurück, falls einer vorhanden ist, oder erstellt einen neuen, falls noch keiner vorhanden ist.- Mit
ViewModelProvider
wird eineViewModel
-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, wirdViewModel
beibehalten, bis das Fragment getrennt wird.
Initialisieren Sie ViewModel
mit der Methode ViewModelProviders.of()
, um ein ViewModelProvider
zu erstellen:
- Initialisieren Sie in der Klasse
GameFragment
die VariableviewModel
. Fügen Sie diesen Code inonCreateView()
nach der Definition der Bindungsvariable ein. Verwenden Sie die MethodeViewModelProviders.of()
und übergeben Sie den zugehörigenGameFragment
-Kontext und dieGameViewModel
-Klasse. - Fügen Sie vor der Initialisierung des
ViewModel
-Objekts eine Log-Anweisung hinzu, um denViewModelProviders.of()
-Methodenaufruf zu protokollieren.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- 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 MethodeonCreateView()
der KlasseGameFragment
die MethodeViewModelProviders.of()
auf, um die KlasseGameViewModel
zu erstellen. Die Logging-Anweisungen, die SieGameFragment
undGameViewModel
hinzugefügt haben, werden in Logcat angezeigt.
- 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, sodassViewModelProviders.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
- Beenden Sie das Spiel oder verlassen Sie das Spiel-Fragment. Das
GameFragment
wird gelöscht. Die zugehörigeGameViewModel
wird ebenfalls zerstört und der CallbackonCleared()
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 inViewModel
verschoben haben, sind alle Daten, die das Fragment zum Anzeigen benötigt, jetztViewModel
.
Wenn die App eine Konfigurationsänderung durchläuft, bleibtViewModel
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
:
- Verschieben Sie die Datenfelder
word
,score
undwordList
. Achten Sie darauf, dassword
undscore
nichtprivate
sind.
Verschieben Sie die BindungsvariableGameFragmentBinding
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. - Verschieben Sie die Methoden
resetList()
undnextWord()
. Mit diesen Methoden wird entschieden, welches Wort auf dem Bildschirm angezeigt wird. - Verschieben Sie die Methodenaufrufe für
resetList()
undnextWord()
in der MethodeonCreateView()
in deninit
-Block vonGameViewModel
.
Diese Methoden müssen sich iminit
-Block befinden, da die Wortliste zurückgesetzt werden sollte, wennViewModel
erstellt wird, nicht jedes Mal, wenn das Fragment erstellt wird. Sie können die Log-Anweisung iminit
-Block vonGameFragment
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:
- Kopieren Sie die Methoden
onSkip()
undonCorrect()
ausGameFragment
inGameViewModel
. - Achten Sie im
GameViewModel
darauf, dass die MethodenonSkip()
undonCorrect()
nichtprivate
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
- Aktualisieren Sie in
GameFragment
die MethodenonSkip()
undonCorrect()
. Entfernen Sie den Code zum Aktualisieren der Punktzahl und rufen Sie stattdessen die entsprechendenonSkip()
- undonCorrect()
-Methoden fürviewModel
auf. - Da Sie die Methode
nextWord()
in die KlasseViewModel
verschoben haben, kann das Spiel-Fragment nicht mehr darauf zugreifen.
Ersetzen Sie inGameFragment
in den MethodenonSkip()
undonCorrect()
den Aufruf vonnextWord()
durchupdateScoreText()
undupdateWordText()
. Mit diesen Methoden werden die Daten auf dem Bildschirm angezeigt.
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- Aktualisieren Sie im
GameFragment
die Variablenscore
undword
, damit dieGameViewModel
-Variablen verwendet werden, da sich diese Variablen jetzt imGameViewModel
befinden.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- Entfernen Sie in
GameViewModel
in der MethodenextWord()
die Aufrufe der MethodenupdateWordText()
undupdateScoreText()
. Diese Methoden werden jetzt überGameFragment
aufgerufen. - Erstellen Sie die App und achten Sie darauf, dass keine Fehler auftreten. Wenn Fehler auftreten, bereinigen Sie das Projekt und erstellen Sie es neu.
- 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).
- Fügen Sie in
GameFragment
eine Methode namensonEndGame()
hinzu. DieonEndGame()
-Methode wird aufgerufen, wenn der Nutzer auf die Schaltfläche Spiel beenden tippt.
private fun onEndGame() {
}
- Suchen Sie in
GameFragment
in der MethodeonCreateView()
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 Bindungsvariablebinding
. Rufen Sie im Click-Listener die MethodeonEndGame()
auf.
binding.endGameButton.setOnClickListener { onEndGame() }
- Fügen Sie in
GameFragment
eine Methode namensgameFinished()
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)
}
- Rufen Sie in der Methode
onEndGame()
die MethodegameFinished()
auf.
private fun onEndGame() {
gameFinished()
}
- 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
.
- Erstellen Sie unter dem Paket
score
eine neue Kotlin-Klasse mit dem NamenScoreViewModel
. Diese Klasse ist dieViewModel
für das Score-Fragment. - Erweitern Sie die Klasse
ScoreViewModel
vonViewModel.
. Fügen Sie einen Konstruktorparameter für die endgültige Punktzahl hinzu. Fügen Sie eineninit
-Block mit einer Log-Anweisung hinzu. - Fügen Sie in der Klasse
ScoreViewModel
eine Variable namensscore
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")
}
}
- Erstellen Sie unter dem Paket
score
eine weitere Kotlin-Klasse namensScoreViewModelFactory
. Diese Klasse ist für die Instanziierung desScoreViewModel
-Objekts verantwortlich. - Erweitern Sie die Klasse
ScoreViewModelFactory
ausViewModelProvider.Factory
. Fügen Sie einen Konstruktorparameter für die endgültige Punktzahl hinzu.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- In
ScoreViewModelFactory
wird in Android Studio ein Fehler zu einem nicht implementierten abstrakten Member angezeigt. Überschreiben Sie die Methodecreate()
, um den Fehler zu beheben. Geben Sie in der Methodecreate()
das neu erstellteScoreViewModel
-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")
}
- Erstellen Sie in
ScoreFragment
Klassenvariablen fürScoreViewModel
undScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- Initialisieren Sie in
ScoreFragment
innerhalb vononCreateView()
nach der Initialisierung der Variablenbinding
dieviewModelFactory
. Verwenden Sie dasScoreViewModelFactory
. Übergeben Sie den endgültigen Wert aus dem Argument-Bundle als Konstruktorparameter anScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
- Initialisieren Sie in
onCreateView(
nach der Initialisierung vonviewModelFactory
das ObjektviewModel
. Rufen Sie die MethodeViewModelProviders.of()
auf und übergeben Sie den zugehörigen Kontext des Score-Fragments undviewModelFactory
. Dadurch wird dasScoreViewModel
-Objekt mit der in der KlasseviewModelFactory
definierten Factory-Methode erstellt..
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- Legen Sie in der Methode
onCreateView()
nach der Initialisierung vonviewModel
den Text der AnsichtscoreText
auf die inScoreViewModel
definierte Endpunktzahl fest.
binding.scoreText.text = viewModel.score.toString()
- 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.
- Optional: Prüfen Sie die
ScoreViewModel
-Logs im Logcat, indem Sie nachScoreViewModel
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
oderFragment
. 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 eineViewModel
ein. - In der Klasse
ViewModel
werden UI-bezogene Daten gespeichert und verwaltet. Mit der KlasseViewModel
können Daten Konfigurationsänderungen wie Bildschirmrotationen überstehen. ViewModel
ist eine der empfohlenen Android-Architekturkomponenten.ViewModelProvider.Factory
ist eine Schnittstelle, mit der Sie einViewModel
-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 | Ein Beispiel für ein |
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, |
Enthält einen Verweis auf die zugehörige | Enthält keinen Verweis auf den zugehörigen UI-Controller. |
Udacity-Kurs:
Android-Entwicklerdokumentation:
- ViewModel – Übersicht
- Umgang mit Lebenszyklen mit lebenszyklusbewussten Komponenten
- Leitfaden zur App-Architektur
ViewModelProvider
ViewModelProvider.Factory
Sonstiges:
- MVVM (Model-View-ViewModel)
- Designprinzip „Separation of Concerns“ (SoC)
- Factory Method Pattern
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.
Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Android Kotlin Fundamentals-Codelabs.