Android Kotlin Fundamentals 05.1: ViewModel i ViewModelFactory

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. Klasa ViewModel 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 obiektu ViewModel, 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

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ę obiektu ViewModel 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

  1. Pobierz kod początkowy aplikacji GuessTheWord i otwórz projekt w Android Studio.
  2. Uruchom aplikację na urządzeniu z Androidem lub w emulatorze.
  3. 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

  1. W Android Studio zapoznaj się z kodem, aby dowiedzieć się, jak działa aplikacja.
  2. 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łowym
  • game/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 metodzie resetList() 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ą metody nextWord().
  • Metoda onCorrect() jest modułem obsługi kliknięć przycisku OK. Ta metoda jest implementowana podobnie do metody onSkip(). 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.

  1. Uruchom kod początkowy i zagraj w grę, wpisując kilka słów. Po każdym słowie kliknij Pomiń lub OK.
  2. 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.
  3. 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.
  4. 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 zwrotnego onSaveInstanceState(). Korzystanie z metody onSaveInstanceState() 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, ViewModelViewModelFactory.

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,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

  1. Otwórz plik build.gradle(module:app). W bloku dependencies dodaj zależność Gradle dla ViewModel.

    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'
  1. W folderze pakietu screens/game/ utwórz nową klasę Kotlin o nazwie GameViewModel.
  2. Spraw, aby klasa GameViewModel rozszerzała klasę abstrakcyjną ViewModel.
  3. Aby pomóc Ci lepiej zrozumieć, jak ViewModel jest uwzględniany w cyklu życia, dodaj blok init 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.

  1. W klasie GameViewModel zastąp metodę onCleared().
  2. Dodaj instrukcję logowania w onCleared(), aby śledzić cykl życia GameViewModel.
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.

  1. W klasie GameFragment dodaj pole typu GameViewModel 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 obiekt ViewModel, 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, element ViewModel jest zachowywany do momentu odłączenia fragmentu.

Zainicjuj ViewModel, używając metody ViewModelProviders.of() do utworzenia ViewModelProvider:

  1. W klasie GameFragment zainicjuj zmienną viewModel. Umieść ten kod w onCreateView() po definicji zmiennej wiązania. Użyj metody ViewModelProviders.of() i przekaż powiązany kontekst GameFragment oraz klasę GameViewModel.
  2. Nad inicjalizacją obiektu ViewModel dodaj instrukcję logowania, aby rejestrować wywołanie metody ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. 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, metoda onCreateView() klasy GameFragment wywołuje metodę ViewModelProviders.of(), aby utworzyć obiekt GameViewModel. Instrukcje logowania dodane do funkcji GameFragmentGameViewModel pojawią się w Logcat.

  1. 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ęc ViewModelProviders.of() jest wywoływany za każdym razem. Obiekt GameViewModel 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
  1. Zakończ grę lub wyjdź z fragmentu gry. GameFragment zostaje zniszczony. Powiązany obiekt GameViewModel również zostanie zniszczony, a wywołanie zwrotne onCleared() 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 do ViewModel:
    wszystkie dane potrzebne do wyświetlenia fragmentu znajdują się teraz w ViewModel. 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:

  1. Przenieś pola danych word, scorewordList. Upewnij się, że wordscore nie są równe private.

    Nie przenoś zmiennej wiążącej GameFragmentBinding, 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.
  2. Przenieś metody resetList()nextWord(). Te metody decydują o tym, jakie słowo ma się wyświetlić na ekranie.
  3. W metodzie onCreateView() przenieś wywołania metod resetList()nextWord() do bloku init metody GameViewModel.

    Te metody muszą znajdować się w bloku init, ponieważ listę słów należy zresetować, gdy tworzony jest ViewModel, a nie za każdym razem, gdy tworzony jest fragment. Instrukcję logowania możesz usunąć w bloku init w sekcji GameFragment.

Obsługa kliknięć onSkip()onCorrect()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:

  1. Skopiuj metody onSkip()onCorrect()GameFragment do GameViewModel.
  2. GameViewModel upewnij się, że metody onSkip()onCorrect() 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

  1. Na stronie GameFragment zaktualizuj formy płatności onSkip()onCorrect(). Usuń kod, aby zaktualizować wynik, i zamiast tego wywołaj odpowiednie metody onSkip()onCorrect() na obiekcie viewModel.
  2. Metoda nextWord() została przeniesiona do klasy ViewModel, więc fragment gry nie ma już do niej dostępu.

    W klasie GameFragment w metodach onSkip()onCorrect() zastąp wywołanie metody nextWord() wywołaniami metod updateScoreText()updateWordText(). Te metody wyświetlają dane na ekranie.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. GameFragment zaktualizuj zmienne scoreword, aby używać zmiennych GameViewModel, ponieważ znajdują się one teraz w GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. GameViewModel w metodzie nextWord() usuń wywołania metod updateWordText()updateScoreText(). Te metody są teraz wywoływane z poziomu GameFragment.
  2. Skompiluj aplikację i upewnij się, że nie ma błędów. Jeśli wystąpią błędy, wyczyść i ponownie skompiluj projekt.
  3. 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ę).

  1. GameFragment dodaj metodę o nazwie onEndGame(). Metoda onEndGame() zostanie wywołana, gdy użytkownik kliknie przycisk Zakończ grę.
private fun onEndGame() {
   }
  1. GameFragment w metodzie onCreateView() znajdź kod, który ustawia odbiorniki kliknięć przycisków OKPomiń. Tuż pod tymi 2 wierszami ustaw odbiornik kliknięć przycisku End Game (Zakończ grę). Użyj zmiennej wiązania binding. W funkcji obsługi kliknięcia wywołaj metodę onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. GameFragment dodaj metodę o nazwie gameFinished(), 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)
}
  1. W metodzie onEndGame() wywołaj metodę gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. 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.

  1. W pakiecie score utwórz nową klasę Kotlin o nazwie ScoreViewModel. Ta klasa będzie ViewModel dla fragmentu wyniku.
  2. Rozszerz klasę ScoreViewModel z klasy ViewModel.. Dodaj parametr konstruktora dla wyniku końcowego. Dodaj blok init z instrukcją logowania.
  3. W klasie ScoreViewModel dodaj zmienną o nazwie score, aby zapisać wynik końcowy.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. W pakiecie score utwórz kolejną klasę Kotlin o nazwie ScoreViewModelFactory. Ta klasa będzie odpowiedzialna za tworzenie instancji obiektu ScoreViewModel.
  2. Przedłuż zajęcia ScoreViewModelFactory od ViewModelProvider.Factory. Dodaj parametr konstruktora dla wyniku końcowego.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ScoreViewModelFactory Android Studio wyświetla błąd dotyczący niezaimplementowanego abstrakcyjnego elementu. Aby naprawić ten błąd, zastąp metodę create(). W metodzie create() zwróć nowo utworzony obiekt ScoreViewModel.
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. W ScoreFragment utwórz zmienne klasy dla ScoreViewModel i ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ScoreFragmentonCreateView() po zainicjowaniu zmiennej binding zainicjuj zmienną viewModelFactory. Użyj ScoreViewModelFactory. Przekaż wynik końcowy z pakietu argumentów jako parametr konstruktora do funkcji ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. W pliku onCreateView( po zainicjowaniu obiektu viewModelFactory zainicjuj obiekt viewModel. Wywołaj metodę ViewModelProviders.of(), przekaż powiązany kontekst fragmentu wyniku i viewModelFactory. Spowoduje to utworzenie obiektu ScoreViewModel za pomocą metody fabrykującej zdefiniowanej w klasie viewModelFactory..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. W metodzie onCreateView() po zainicjowaniu viewModel ustaw tekst widoku scoreText na wynik końcowy zdefiniowany w ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. 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.

  1. Opcjonalnie: sprawdź dzienniki ScoreViewModel w narzędziu Logcat, filtrując je według ScoreViewModel. 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 lub Fragment. 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 w ViewModel.
  • Klasa ViewModel przechowuje dane związane z interfejsem i nimi zarządza. Klasa ViewModel 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ć obiekt ViewModel.

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 ScoreFragment utworzony w tym laboratorium.

Przykładem ViewModel jest ScoreViewModel utworzony w tym laboratorium.

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 ViewModel tak.

Zawiera odwołanie do powiązanego ViewModel.

Nie zawiera odniesienia do powiązanego kontrolera interfejsu.

Kurs Udacity:

Dokumentacja dla deweloperów aplikacji na Androida:

Inne:

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ę: 5.2. LiveData i obserwatorzy LiveData

Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z podstaw języka Kotlin na Androidzie.