Ten moduł Codelab jest częścią kursu Android Kotlin Fundamentals. Najwięcej korzyści przyniesie Ci ukończenie wszystkich ćwiczeń w kolejności. Wszystkie ćwiczenia z tego kursu znajdziesz na stronie docelowej kursu Android Kotlin Fundamentals.
Ekran tytułowy | Ekran gry | Ekran wyników |
Wprowadzenie
W tych ćwiczeniach z programowania dowiesz się więcej o jednym z komponentów architektury Androida: ViewModel
.
- Klasy
ViewModel
używasz do przechowywania danych związanych z interfejsem i zarządzania nimi w sposób uwzględniający cykl życia. KlasaViewModel
umożliwia zachowanie danych po zmianach konfiguracji urządzenia, takich jak obracanie ekranu czy zmiany dostępności klawiatury. - Klasy
ViewModelFactory
używasz do tworzenia instancji i zwracania obiektuViewModel
, który przetrwa zmiany konfiguracji.
Co warto wiedzieć
- Jak tworzyć podstawowe aplikacje na Androida w języku Kotlin.
- Jak używać wykresu nawigacji do implementowania nawigacji w aplikacji.
- Jak dodać kod, który umożliwia przechodzenie między miejscami docelowymi aplikacji i przekazywanie danych między nimi.
- Jak działają cykle życia aktywności i fragmentów.
- Jak dodać informacje o logowaniu do aplikacji i odczytywać logi za pomocą Logcat w Android Studio.
Czego się nauczysz
- Jak korzystać z zalecanej architektury aplikacji na Androida.
- Jak używać w aplikacji klas
Lifecycle
,ViewModel
iViewModelFactory
. - Jak zachować dane interfejsu podczas zmian konfiguracji urządzenia.
- Czym jest wzorzec projektowy metody fabrycznej i jak go używać.
- Jak utworzyć obiekt
ViewModel
za pomocą interfejsuViewModelProvider.Factory
.
Jakie zadania wykonasz
- Dodaj do aplikacji
ViewModel
, aby zapisać dane aplikacji i zachować je po zmianach konfiguracji. - Użyj
ViewModelFactory
i wzorca projektowego metody fabrykującej, aby utworzyć instancję obiektuViewModel
z parametrami konstruktora.
W ćwiczeniach z programowania w lekcji 5 tworzysz aplikację GuessTheWord, zaczynając od kodu początkowego. GuessTheWord to gra w kalambury dla 2 osób, w której gracze współpracują, aby uzyskać jak najwyższy wynik.
Pierwszy gracz patrzy na słowa w aplikacji i kolejno odgrywa każde z nich, uważając, aby nie pokazać słowa drugiemu graczowi. Drugi gracz próbuje odgadnąć słowo.
Aby rozpocząć grę, pierwszy gracz otwiera aplikację na urządzeniu i widzi słowo, np. „gitara”, jak pokazano na zrzucie ekranu poniżej.
Pierwszy gracz odgrywa słowo, uważając, aby go nie wypowiedzieć.
- Gdy drugi gracz odgadnie słowo, pierwszy gracz naciśnie przycisk Got It (Mam to), co zwiększy liczbę o 1 i wyświetli kolejne słowo.
- Jeśli drugi gracz nie odgadnie słowa, pierwszy gracz naciśnie przycisk Pomiń, co zmniejszy liczbę o 1 i spowoduje przejście do następnego słowa.
- Aby zakończyć grę, naciśnij przycisk Zakończ grę. (Ta funkcja nie jest dostępna w kodzie początkowym pierwszego laboratorium w tej serii).
W tym zadaniu pobierzesz i uruchomisz aplikację początkową oraz sprawdzisz kod.
Krok 1. Pierwsze kroki
- Pobierz kod początkowy aplikacji GuessTheWord i otwórz projekt w Android Studio.
- Uruchom aplikację na urządzeniu z Androidem lub w emulatorze.
- Naciśnij przyciski. Zauważ, że przycisk Pomiń wyświetla następne słowo i zmniejsza wynik o 1, a przycisk Rozumiem wyświetla następne słowo i zwiększa wynik o 1. Przycisk Zakończ grę nie jest zaimplementowany, więc po jego kliknięciu nic się nie dzieje.
Krok 2. Przejrzyj kod
- W Android Studio zapoznaj się z kodem, aby dowiedzieć się, jak działa aplikacja.
- Zapoznaj się z opisanymi poniżej plikami, które są szczególnie ważne.
MainActivity.kt
Ten plik zawiera tylko domyślny kod wygenerowany przez szablon.
res/layout/main_activity.xml
Ten plik zawiera główny układ aplikacji. NavHostFragment
hostuje pozostałe fragmenty, gdy użytkownik porusza się po aplikacji.
Fragmenty interfejsu
Kod początkowy zawiera 3 fragmenty w 3 różnych pakietach w pakiecie com.example.android.guesstheword.screens
:
title/TitleFragment
na ekranie tytułowymgame/GameFragment
– ekran gry.score/ScoreFragment
– ekran wyników
screens/title/TitleFragment.kt
Fragment tytułu to pierwszy ekran, który jest wyświetlany po uruchomieniu aplikacji. Do przycisku Graj przypisany jest moduł obsługi kliknięć, który umożliwia przejście do ekranu gry.
screens/game/GameFragment.kt
To główny fragment, w którym rozgrywa się większość akcji w grze:
- Zmienne są zdefiniowane dla bieżącego słowa i bieżącego wyniku.
wordList
zdefiniowana w metodzieresetList()
to przykładowa lista słów, które mają być używane w grze.- Metoda
onSkip()
to moduł obsługi kliknięć przycisku Pomiń. Zmniejsza wynik o 1, a następnie wyświetla kolejne słowo za pomocą metodynextWord()
. - Metoda
onCorrect()
jest modułem obsługi kliknięć przycisku OK. Ta metoda jest implementowana podobnie do metodyonSkip()
. Jedyna różnica polega na tym, że ta metoda dodaje 1 punkt do wyniku zamiast go odejmować.
screens/score/ScoreFragment.kt
ScoreFragment
to ostatni ekran w grze, na którym wyświetla się ostateczny wynik gracza. W tym laboratorium kodowania dodasz implementację, która będzie wyświetlać ten ekran i pokazywać wynik końcowy.
res/navigation/main_navigation.xml
Graf nawigacji pokazuje, jak fragmenty są połączone za pomocą nawigacji:
- Z fragmentu tytułu użytkownik może przejść do fragmentu gry.
- Z fragmentu gry użytkownik może przejść do fragmentu wyniku.
- Z fragmentu z wynikami użytkownik może wrócić do fragmentu z grą.
W tym zadaniu znajdziesz problemy z aplikacją startową GuessTheWord.
- Uruchom kod początkowy i zagraj w grę, wpisując kilka słów. Po każdym słowie kliknij Pomiń lub OK.
- Na ekranie gry pojawi się słowo i aktualny wynik. Zmień orientację ekranu, obracając urządzenie lub emulator. Zwróć uwagę, że bieżący wynik został utracony.
- Zagraj jeszcze kilka razy. Gdy na ekranie gry pojawi się wynik, zamknij i ponownie otwórz aplikację. Zauważ, że gra rozpoczyna się od początku, ponieważ stan aplikacji nie jest zapisywany.
- Zagraj w grę, wpisując kilka słów, a potem kliknij przycisk Zakończ grę. Zauważ, że nic się nie dzieje.
Problemy w aplikacji:
- Aplikacja startowa nie zapisuje i nie przywraca stanu aplikacji podczas zmian konfiguracji, np. gdy zmienia się orientacja urządzenia lub gdy aplikacja jest zamykana i uruchamiana ponownie.
Ten problem możesz rozwiązać za pomocą wywołania zwrotnegoonSaveInstanceState()
. Korzystanie z metodyonSaveInstanceState()
wymaga jednak napisania dodatkowego kodu, który zapisuje stan w pakiecie i wdraża logikę pobierania tego stanu. Poza tym ilość danych, które można przechowywać, jest minimalna. - Gdy użytkownik kliknie przycisk Zakończ grę, ekran gry nie przechodzi do ekranu z wynikami.
Możesz rozwiązać te problemy, korzystając z komponentów architektury aplikacji, które poznasz w tym ćwiczeniu.
Architektura aplikacji
Architektura aplikacji to sposób projektowania klas aplikacji i relacji między nimi, dzięki któremu kod jest uporządkowany, dobrze działa w określonych scenariuszach i jest łatwy w użyciu. W tym zestawie 4 ćwiczeń z programowania ulepszenia, które wprowadzisz w aplikacji GuessTheWord, będą zgodne z wytycznymi dotyczącymi architektury aplikacji na Androida. Będziesz też używać komponentów architektury Androida. Architektura aplikacji na Androida jest podobna do wzorca architektonicznego MVVM (model-view-viewmodel).
Aplikacja GuessTheWord jest zgodna z zasadą projektowania rozdzielenia odpowiedzialności i jest podzielona na klasy, z których każda odpowiada za inną funkcję. W tym pierwszym module szkoleniowym lekcji klasy, z którymi będziesz pracować, to kontroler interfejsu, ViewModel
i ViewModelFactory
.
kontroler interfejsu,
Kontroler interfejsu to klasa oparta na interfejsie, np. Activity
lub Fragment
. Kontroler interfejsu powinien zawierać tylko logikę obsługującą interakcje z interfejsem i systemem operacyjnym, takie jak wyświetlanie widoków i przechwytywanie danych wprowadzanych przez użytkownika. Nie umieszczaj w kontrolerze interfejsu logiki podejmowania decyzji, np. logiki określającej tekst do wyświetlenia.
W kodzie początkowym aplikacji GuessTheWord kontrolerami interfejsu są 3 fragmenty: GameFragment
, ScoreFragment,
i TitleFragment
. Zgodnie z zasadą projektowania „rozdzielenia odpowiedzialności” klasa GameFragment
odpowiada tylko za rysowanie elementów gry na ekranie i wykrywanie, kiedy użytkownik dotyka przycisków. Gdy użytkownik kliknie przycisk, te informacje zostaną przekazane do GameViewModel
.
ViewModel
ViewModel
zawiera dane, które mają być wyświetlane we fragmencie lub aktywności powiązanej z ViewModel
. ViewModel
może wykonywać proste obliczenia i transformacje danych, aby przygotować je do wyświetlenia przez kontroler interfejsu. W tej architekturze ViewModel
podejmuje decyzje.GameViewModel
przechowuje dane, takie jak wartość wyniku, lista słów i bieżące słowo, ponieważ są to dane, które mają być wyświetlane na ekranie. GameViewModel
zawiera też logikę biznesową do wykonywania prostych obliczeń, które pozwalają określić bieżący stan danych.
ViewModelFactory
Obiekt ViewModelFactory
tworzy instancje obiektów ViewModel
z parametrami konstruktora lub bez nich.
W dalszych ćwiczeniach dowiesz się więcej o innych komponentach architektury Androida, które są powiązane z kontrolerami interfejsu i ViewModel
.
Klasa ViewModel
służy do przechowywania danych związanych z interfejsem i zarządzania nimi. W tej aplikacji każdy ViewModel
jest powiązany z 1 fragmentem.
W tym zadaniu dodasz do aplikacji pierwszy ViewModel
, czyli GameViewModel
dla GameFragment
. Dowiesz się też, co oznacza, że ViewModel
jest świadomy cyklu życia.
Krok 1. Dodaj klasę GameViewModel
- Otwórz plik
build.gradle(module:app)
. W blokudependencies
dodaj zależność Gradle dlaViewModel
.
Jeśli używasz najnowszej wersji biblioteki, aplikacja z rozwiązaniem powinna się skompilować zgodnie z oczekiwaniami. Jeśli nie, spróbuj rozwiązać problem lub wróć do wersji podanej poniżej.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- W folderze pakietu
screens/game/
utwórz nową klasę Kotlin o nazwieGameViewModel
. - Spraw, aby klasa
GameViewModel
rozszerzała klasę abstrakcyjnąViewModel
. - Aby pomóc Ci lepiej zrozumieć, jak
ViewModel
jest uwzględniany w cyklu życia, dodaj blokinit
z instrukcjąlog
.
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
Krok 2. Zastąp metodę onCleared() i dodaj rejestrowanie
Obiekt ViewModel
jest niszczony, gdy powiązany z nim fragment zostanie odłączony lub gdy aktywność się zakończy. Tuż przed zniszczeniem obiektu ViewModel
wywoływane jest wywołanie zwrotne onCleared()
, aby wyczyścić zasoby.
- W klasie
GameViewModel
zastąp metodęonCleared()
. - Dodaj instrukcję logowania w
onCleared()
, aby śledzić cykl życiaGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
Krok 3. Powiąż GameViewModel z fragmentem gry
ViewModel
musi być powiązany z kontrolerem interfejsu. Aby je ze sobą powiązać, utwórz odwołanie do elementu ViewModel
w kontrolerze interfejsu.
W tym kroku utworzysz odwołanie do elementu GameViewModel
w odpowiednim kontrolerze interfejsu, czyli GameFragment
.
- W klasie
GameFragment
dodaj pole typuGameViewModel
na najwyższym poziomie jako zmienną klasy.
private lateinit var viewModel: GameViewModel
Krok 4. Zainicjuj ViewModel
Podczas zmian konfiguracji, takich jak obracanie ekranu, kontrolery interfejsu, np. fragmenty, są ponownie tworzone. Jednak ViewModel
instancji przetrwa. Jeśli utworzysz instancję ViewModel
za pomocą klasy ViewModel
, za każdym razem, gdy fragment zostanie ponownie utworzony, powstanie nowy obiekt. Zamiast tego utwórz instancję ViewModel
za pomocą ViewModelProvider
.
Jak działa ViewModelProvider
:
- Funkcja
ViewModelProvider
zwraca istniejący obiektViewModel
, jeśli taki istnieje, lub tworzy nowy, jeśli jeszcze nie istnieje. ViewModelProvider
tworzy instancjęViewModel
powiązaną z danym zakresem (aktywnością lub fragmentem).- Utworzony obiekt
ViewModel
jest przechowywany tak długo, jak długo istnieje zakres. Jeśli na przykład zakres to fragment, elementViewModel
jest zachowywany do momentu odłączenia fragmentu.
Zainicjuj ViewModel
, używając metody ViewModelProviders.of()
do utworzenia ViewModelProvider
:
- W klasie
GameFragment
zainicjuj zmiennąviewModel
. Umieść ten kod wonCreateView()
po definicji zmiennej wiązania. Użyj metodyViewModelProviders.of()
i przekaż powiązany kontekstGameFragment
oraz klasęGameViewModel
. - Nad inicjalizacją obiektu
ViewModel
dodaj instrukcję logowania, aby rejestrować wywołanie metodyViewModelProviders.of()
.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- Uruchom aplikację. W Android Studio otwórz panel Logcat i filtruj według
Game
. Kliknij przycisk Odtwórz na urządzeniu lub emulatorze. Otworzy się ekran gry.
Jak widać w Logcat, metodaonCreateView()
klasyGameFragment
wywołuje metodęViewModelProviders.of()
, aby utworzyć obiektGameViewModel
. Instrukcje logowania dodane do funkcjiGameFragment
iGameViewModel
pojawią się w Logcat.
- Włącz ustawienie autoobracania na urządzeniu lub emulatorze i kilka razy zmień orientację ekranu.
GameFragment
jest za każdym razem niszczony i tworzony ponownie, więcViewModelProviders.of()
jest wywoływany za każdym razem. ObiektGameViewModel
jest jednak tworzony tylko raz i nie jest ponownie tworzony ani niszczony przy każdym wywołaniu.
I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of
- Zakończ grę lub wyjdź z fragmentu gry.
GameFragment
zostaje zniszczony. Powiązany obiektGameViewModel
również zostanie zniszczony, a wywołanie zwrotneonCleared()
zostanie wywołane.
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
przetrwa zmiany konfiguracji, więc jest dobrym miejscem na dane, które muszą przetrwać zmiany konfiguracji:
- Umieść dane, które mają być wyświetlane na ekranie, oraz kod do ich przetwarzania w
ViewModel
. - Obiekt
ViewModel
nigdy nie powinien zawierać odwołań do fragmentów, aktywności ani widoków, ponieważ aktywności, fragmenty i widoki nie przetrwają zmian konfiguracji.
Dla porównania zobacz, jak dane interfejsu GameFragment
są obsługiwane w aplikacji startowej przed dodaniem ViewModel
i po dodaniu ViewModel
:
- Przed dodaniem
ViewModel
:
gdy w aplikacji nastąpi zmiana konfiguracji, np. obrócenie ekranu, fragment gry zostanie zniszczony i utworzony ponownie. Dane zostaną utracone. - Po dodaniu
ViewModel
i przeniesieniu danych interfejsu fragmentu gry doViewModel
:
wszystkie dane potrzebne do wyświetlenia fragmentu znajdują się teraz wViewModel
. Gdy aplikacja przechodzi zmianę konfiguracji,ViewModel
pozostaje bez zmian, a dane są zachowywane.
W tym zadaniu przeniesiesz dane interfejsu aplikacji do klasy GameViewModel
wraz z metodami przetwarzania danych. Dzięki temu dane są zachowywane podczas zmian konfiguracji.
Krok 1. Przenieś pola danych i przetwarzanie danych do ViewModel
Przenieś te pola danych i metody z GameFragment
do GameViewModel
:
- Przenieś pola danych
word
,score
iwordList
. Upewnij się, żeword
iscore
nie są równeprivate
.
Nie przenoś zmiennej wiążącejGameFragmentBinding
, ponieważ zawiera ona odniesienia do widoków. Ta zmienna służy do rozszerzania układu, konfigurowania odbiorników kliknięć i wyświetlania danych na ekranie – są to zadania fragmentu. - Przenieś metody
resetList()
inextWord()
. Te metody decydują o tym, jakie słowo ma się wyświetlić na ekranie. - W metodzie
onCreateView()
przenieś wywołania metodresetList()
inextWord()
do blokuinit
metodyGameViewModel
.
Te metody muszą znajdować się w blokuinit
, ponieważ listę słów należy zresetować, gdy tworzony jestViewModel
, a nie za każdym razem, gdy tworzony jest fragment. Instrukcję logowania możesz usunąć w blokuinit
w sekcjiGameFragment
.
Obsługa kliknięć onSkip()
i onCorrect()
w GameFragment
zawiera kod do przetwarzania danych i aktualizowania interfejsu. Kod aktualizujący interfejs użytkownika powinien pozostać we fragmencie, ale kod przetwarzający dane musi zostać przeniesiony do ViewModel
.
Na razie umieść identyczne metody w obu miejscach:
- Skopiuj metody
onSkip()
ionCorrect()
zGameFragment
doGameViewModel
. - W
GameViewModel
upewnij się, że metodyonSkip()
ionCorrect()
nie sąprivate
, ponieważ będziesz się do nich odwoływać z fragmentu.
Oto kod klasy GameViewModel
po refaktoryzacji:
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!")
}
}
Oto kod klasy GameFragment
po refaktoryzacji:
/**
* 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()
}
}
Krok 2. Zaktualizuj odwołania do funkcji obsługi kliknięć i pól danych w klasie GameFragment
- Na stronie
GameFragment
zaktualizuj formy płatnościonSkip()
ionCorrect()
. Usuń kod, aby zaktualizować wynik, i zamiast tego wywołaj odpowiednie metodyonSkip()
ionCorrect()
na obiekcieviewModel
. - Metoda
nextWord()
została przeniesiona do klasyViewModel
, więc fragment gry nie ma już do niej dostępu.
W klasieGameFragment
w metodachonSkip()
ionCorrect()
zastąp wywołanie metodynextWord()
wywołaniami metodupdateScoreText()
iupdateWordText()
. Te metody wyświetlają dane na ekranie.
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- W
GameFragment
zaktualizuj zmiennescore
iword
, aby używać zmiennychGameViewModel
, ponieważ znajdują się one teraz wGameViewModel
.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- W
GameViewModel
w metodzienextWord()
usuń wywołania metodupdateWordText()
iupdateScoreText()
. Te metody są teraz wywoływane z poziomuGameFragment
. - Skompiluj aplikację i upewnij się, że nie ma błędów. Jeśli wystąpią błędy, wyczyść i ponownie skompiluj projekt.
- Uruchom aplikację i zagraj w grę, używając kilku słów. Na ekranie gry obróć urządzenie. Zwróć uwagę, że po zmianie orientacji ekranu bieżący wynik i bieżące słowo pozostają bez zmian.
Brawo! Teraz wszystkie dane aplikacji są przechowywane w ViewModel
, więc są zachowywane podczas zmian konfiguracji.
W tym zadaniu zaimplementujesz detektor kliknięć przycisku End Game (Zakończ grę).
- W
GameFragment
dodaj metodę o nazwieonEndGame()
. MetodaonEndGame()
zostanie wywołana, gdy użytkownik kliknie przycisk Zakończ grę.
private fun onEndGame() {
}
- W
GameFragment
w metodzieonCreateView()
znajdź kod, który ustawia odbiorniki kliknięć przycisków OK i Pomiń. Tuż pod tymi 2 wierszami ustaw odbiornik kliknięć przycisku End Game (Zakończ grę). Użyj zmiennej wiązaniabinding
. W funkcji obsługi kliknięcia wywołaj metodęonEndGame()
.
binding.endGameButton.setOnClickListener { onEndGame() }
- W
GameFragment
dodaj metodę o nazwiegameFinished()
, która przeniesie użytkownika do ekranu z wynikiem. Przekaż wynik jako argument, używając 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)
}
- W metodzie
onEndGame()
wywołaj metodęgameFinished()
.
private fun onEndGame() {
gameFinished()
}
- Uruchom aplikację, zagraj w grę i przejrzyj kilka słów. Kliknij przycisk Zakończ grę . Zauważ, że aplikacja przechodzi do ekranu z wynikami, ale nie wyświetla ostatecznego wyniku. Naprawisz to w następnym zadaniu.
Gdy użytkownik zakończy grę, ScoreFragment
nie wyświetla wyniku. Chcesz, aby ViewModel
przechowywał wynik, który ma być wyświetlany przez ScoreFragment
. Wartość wyniku przekażesz podczas inicjowania ViewModel
za pomocą wzorca metody fabrycznej.
Wzorzec metody fabrycznej to wzorzec projektowy tworzenia, który do tworzenia obiektów używa metod fabrycznych. Metoda fabrykująca to metoda, która zwraca instancję tej samej klasy.
W tym zadaniu utworzysz ViewModel
ze sparametryzowanym konstruktorem fragmentu wyniku i metodą fabryczną do tworzenia instancji ViewModel
.
- W pakiecie
score
utwórz nową klasę Kotlin o nazwieScoreViewModel
. Ta klasa będzieViewModel
dla fragmentu wyniku. - Rozszerz klasę
ScoreViewModel
z klasyViewModel.
. Dodaj parametr konstruktora dla wyniku końcowego. Dodaj blokinit
z instrukcją logowania. - W klasie
ScoreViewModel
dodaj zmienną o nazwiescore
, aby zapisać wynik końcowy.
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
- W pakiecie
score
utwórz kolejną klasę Kotlin o nazwieScoreViewModelFactory
. Ta klasa będzie odpowiedzialna za tworzenie instancji obiektuScoreViewModel
. - Przedłuż zajęcia
ScoreViewModelFactory
odViewModelProvider.Factory
. Dodaj parametr konstruktora dla wyniku końcowego.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- W
ScoreViewModelFactory
Android Studio wyświetla błąd dotyczący niezaimplementowanego abstrakcyjnego elementu. Aby naprawić ten błąd, zastąp metodęcreate()
. W metodziecreate()
zwróć nowo utworzony obiektScoreViewModel
.
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")
}
- W
ScoreFragment
utwórz zmienne klasy dlaScoreViewModel
iScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- W
ScoreFragment
wonCreateView()
po zainicjowaniu zmiennejbinding
zainicjuj zmiennąviewModelFactory
. UżyjScoreViewModelFactory
. Przekaż wynik końcowy z pakietu argumentów jako parametr konstruktora do funkcjiScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
- W pliku
onCreateView(
po zainicjowaniu obiektuviewModelFactory
zainicjuj obiektviewModel
. Wywołaj metodęViewModelProviders.of()
, przekaż powiązany kontekst fragmentu wyniku iviewModelFactory
. Spowoduje to utworzenie obiektuScoreViewModel
za pomocą metody fabrykującej zdefiniowanej w klasieviewModelFactory
..
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- W metodzie
onCreateView()
po zainicjowaniuviewModel
ustaw tekst widokuscoreText
na wynik końcowy zdefiniowany wScoreViewModel
.
binding.scoreText.text = viewModel.score.toString()
- Uruchom aplikację i zagraj w grę. Przejrzyj niektóre lub wszystkie słowa i kliknij Zakończ grę. Zwróć uwagę, że fragment wyniku wyświetla teraz wynik końcowy.
- Opcjonalnie: sprawdź dzienniki
ScoreViewModel
w narzędziu Logcat, filtrując je wedługScoreViewModel
. Powinna być wyświetlana wartość oceny.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15
W tym zadaniu zaimplementowano ScoreFragment
, aby używać ViewModel
. Dowiedzieliśmy się też, jak utworzyć konstruktor sparametryzowany dla ViewModel
za pomocą interfejsu ViewModelFactory
.
Gratulacje! Zmieniono architekturę aplikacji, aby korzystać z jednego ze składników architektury Androida, ViewModel
. Problem z cyklem życia aplikacji został rozwiązany i dane gry są teraz zachowywane po zmianach konfiguracji. Dowiedzieliśmy się też, jak utworzyć konstruktor sparametryzowany do tworzenia obiektu ViewModel
za pomocą interfejsu ViewModelFactory
.
Projekt Android Studio: GuessTheWord
- Wskazówki dotyczące architektury aplikacji na Androida zalecają rozdzielanie klas o różnych zadaniach.
- Kontroler interfejsu to klasa oparta na interfejsie, np.
Activity
lubFragment
. Kontrolery interfejsu powinny zawierać tylko logikę obsługującą interakcje z interfejsem i systemem operacyjnym. Nie powinny zawierać danych, które mają być wyświetlane w interfejsie. Umieść te dane wViewModel
. - Klasa
ViewModel
przechowuje dane związane z interfejsem i nimi zarządza. KlasaViewModel
umożliwia przetrwanie danych po zmianach konfiguracji, takich jak obracanie ekranu. ViewModel
jest jednym z zalecanych składników architektury Androida.ViewModelProvider.Factory
to interfejs, za pomocą którego możesz utworzyć obiektViewModel
.
W tabeli poniżej znajdziesz porównanie kontrolerów interfejsu z instancjami ViewModel
, które przechowują dane na ich potrzeby:
Kontroler interfejsu | ViewModel |
Przykładem kontrolera interfejsu jest element | Przykładem |
Nie zawiera żadnych danych do wyświetlenia w interfejsie. | Zawiera dane, które kontroler interfejsu wyświetla w interfejsie. |
Zawiera kod do wyświetlania danych i kod zdarzeń użytkownika, np. odbiorniki kliknięć. | Zawiera kod do przetwarzania danych. |
Niszczone i tworzone ponownie przy każdej zmianie konfiguracji. | Niszczony tylko wtedy, gdy powiązany kontroler interfejsu znika na stałe – w przypadku aktywności, gdy się kończy, a w przypadku fragmentu, gdy jest odłączany. |
Zawiera widoki. | Nigdy nie powinny zawierać odwołań do aktywności, fragmentów ani widoków, ponieważ nie przetrwają one zmian konfiguracji, ale |
Zawiera odwołanie do powiązanego | Nie zawiera odniesienia do powiązanego kontrolera interfejsu. |
Kurs Udacity:
Dokumentacja dla deweloperów aplikacji na Androida:
- Omówienie ViewModel
- Obsługa cykli życia za pomocą komponentów uwzględniających cykl życia
- Przewodnik po architekturze aplikacji
ViewModelProvider
ViewModelProvider.Factory
Inne:
- Wzorzec architektury MVVM (model-view-viewmodel).
- Zasada projektowania rozdzielenia odpowiedzialności (SoC)
- Wzorzec metody fabrycznej
W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:
- W razie potrzeby przypisz pracę domową.
- Poinformuj uczniów, jak przesyłać projekty.
- Oceń zadania domowe.
Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.
Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.
Odpowiedz na te pytania
Pytanie 1
Aby uniknąć utraty danych podczas zmiany konfiguracji urządzenia, w której klasie należy zapisać dane aplikacji?
ViewModel
LiveData
Fragment
Activity
Pytanie 2
ViewModel
nie powinien nigdy zawierać odniesień do fragmentów, aktywności ani widoków. Prawda czy fałsz?
- Prawda
- Fałsz
Pytanie 3
Kiedy ViewModel
jest niszczony?
- Gdy powiązany kontroler interfejsu jest niszczony i tworzony ponownie podczas zmiany orientacji urządzenia.
- w przypadku zmiany orientacji,
- Gdy powiązany kontroler interfejsu zakończy działanie (jeśli jest to aktywność) lub zostanie odłączony (jeśli jest to fragment).
- Gdy użytkownik naciśnie przycisk Wstecz.
Pytanie 4
Do czego służy interfejs ViewModelFactory
?
- Utwórz instancję obiektu
ViewModel
. - Zachowywanie danych podczas zmiany orientacji.
- Odświeżanie danych wyświetlanych na ekranie.
- otrzymywać powiadomienia, gdy dane aplikacji zostaną zmienione;
Rozpocznij kolejną lekcję:
Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z podstaw języka Kotlin na Androidzie.