Android Kotlin Fundamentals 05.1: ViewModel i ViewModelFactory

Te ćwiczenia są częścią kursu Android Kotlin Fundamentals. Skorzystaj z tego kursu, jeśli będziesz wykonywać kolejno kilka ćwiczeń z programowania. Wszystkie ćwiczenia z kursu są wymienione na stronie docelowej ćwiczeń z programowania na temat Kotlin.

Ekran tytułowy

Ekran gry

Ekran wyniku

Wstęp

Dzięki ćwiczeniom z programowania poznasz jeden z komponentów architektury Androida: ViewModel:

  • Klasa ViewModel służy do przechowywania danych związanych z interfejsem użytkownika i zarządzania nimi w sposób uwzględniający cykl życia konta. Klasa ViewModel umożliwia przetrwanie zmian konfiguracji urządzenia, np. obrotu ekranu i zmiany dostępności klawiatury.
  • Aby utworzyć instancję i zwrócić obiekt ViewModel, który przeżyje zmiany konfiguracji, użyj klasy ViewModelFactory.

Co musisz wiedzieć

  • Jak tworzyć podstawowe aplikacje na Androida w Kotlinie.
  • Jak za pomocą wykresu nawigacyjnego zaimplementować nawigację w aplikacji.
  • Jak dodać kod, aby poruszać się między miejscami docelowymi aplikacji i przekazywać dane między miejscami docelowymi.
  • Jak działa cykl życia aktywności i fragmentu.
  • Jak dodać dane logowania do aplikacji i odczytać logi przy użyciu LogCat w Android Studio.

Czego się nauczysz

  • Dowiedz się, jak używać zalecanej architektury aplikacji na Androida.
  • Jak korzystać z zajęć Lifecycle, ViewModel i ViewModelFactory w aplikacji.
  • Jak zachować dane w interfejsie po wprowadzeniu zmian w konfiguracji urządzenia
  • Co to jest wzór metody fabrycznej i jak go używać.
  • Jak utworzyć obiekt ViewModel za pomocą interfejsu ViewModelProvider.Factory.

Jakie zadania wykonasz:

  • Dodaj ViewModel do aplikacji, aby zapisywać dane aplikacji i przetrwać konfigurację.
  • Za pomocą ViewModelFactory i wzorca projektowania fabrycznego możesz utworzyć obiekt ViewModel z parametrami konstruktora.

W ćwiczeniach z lekcji na lekcji 5 tworzysz aplikację GuessTheWord, zaczynając od kodu początkowego. GuessTheWord to 2-osobowa gra w stylu charades, w której gracze współpracują, aby osiągnąć najwyższy możliwy wynik.

Pierwszy gracz sprawdza słowa w aplikacji i wykonuje po kolei każdy z nich, starając się nie wyświetlać słów drugiemu graczowi. Drugi gracz próbuje odgadnąć słowo.

Aby zagrać w grę, pierwszy gracz otworzy aplikację na urządzeniu i zobaczy słowo, na przykład "gitara&&quot, jak na zrzucie ekranu poniżej.

Gracz wypowiada słowo, pamiętając, aby nie mówić samego słowa.

  • Gdy drugi gracz odgadnie słowo poprawnie, pierwszy naciśnij przycisk OK, co zwiększa liczbę o jeden i wyświetla następne słowo.
  • Jeśli drugi gracz nie odgadnie słowa, pierwszy z nich naciśnie przycisk Pomiń, co zmniejszy liczbę o jeden i przeskoczy do następnego.
  • Aby zakończyć grę, naciśnij przycisk Zakończ grę. (Ta funkcja nie jest w kodzie początkowym pierwszego modułu ćwiczeń z serii).

W tym zadaniu pobierzesz i uruchomisz aplikację startową oraz sprawdzisz kod.

Krok 1. Rozpocznij

  1. Pobierz kod startowy GuessTheWord i otwórz projekt w Android Studio.
  2. Uruchom aplikację na urządzeniu z Androidem lub w emulatorze.
  3. Kliknij przyciski. Pamiętaj, że przycisk Pomiń wyświetla następne słowo i obniża wynik o jeden, a przycisk OK pokazuje kolejne słowo i zwiększa wynik o jeden. Przycisk Zakończ grę nie jest zaimplementowany, więc nic się nie dzieje, gdy go klikniesz.

Krok 2. Wykonaj kod

  1. Zapoznaj się z kodem, aby dowiedzieć się, jak działa aplikacja w Android Studio.
  2. Pamiętaj, aby przyjrzeć się plikom opisanym poniżej, 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 przechowuje inne fragmenty, gdy użytkownik porusza się po aplikacji.

Fragmenty interfejsu

Kod startowy zawiera 3 fragmenty w 3 różnych pakietach w pakiecie com.example.android.guesstheword.screens:

  • title/TitleFragment na ekranie tytułowym
  • game/GameFragment na ekran gry
  • score/ScoreFragment na ekranie wyniku

ekrany/tytuł/TytułFragment.kt

Fragment tytułu to pierwszy ekran wyświetlany po uruchomieniu aplikacji. Moduł obsługi kliknięcia to przycisk Zagraj, który pozwala przejść do ekranu gry.

ekrany/gry/GameFragment.kt

To jest główny fragment, w którym większość działania ma miejsce:

  • Zmienne są zdefiniowane dla bieżącego słowa i bieżącego wyniku.
  • wordList zdefiniowana w metodzie resetList() to przykładowa lista słów, których należy użyć w grze.
  • Metoda onSkip() to moduł do obsługi kliknięć przycisku Pomiń. Zmniejsza wynik o 1, a następnie wyświetla następne słowo za pomocą metody nextWord().
  • Metoda onCorrect() to moduł do obsługi kliknięć przycisku OK. Ta metoda jest zaimplementowana podobnie do metody onSkip(). Jedyna różnica polega na tym, że ta metoda dodaje wynik do wyniku zamiast odejmować.

ekrany/wynik/ScoreFragment.kt

ScoreFragment to ostatni ekran w grze, w którym wyświetla się końcowy wynik gracza. W ćwiczeniach z programowania dodajesz implementację, aby wyświetlać ten ekran i wyświetlać końcowy wynik.

res/Nawigacja/glowna_nawigacja.xml

Wykres nawigacji pokazuje sposób łączenia fragmentów za pomocą nawigacji:

  • Z fragmentu tytułu użytkownik może przejść do fragmentu gry.
  • Z fragmentu z gry użytkownik może przejść do fragmentu wyniku.
  • Po kliknięciu fragmentu wyniku użytkownik może wrócić do fragmentu gry.

W tym zadaniu znajdziesz problemy z aplikacją GuessTheWord Starter.

  1. Uruchom kod startowy i rozpocznij grę po kilku słowach, klikając Pomiń lub OK za każdym słowem.
  2. Na ekranie gry będzie teraz widoczne słowo i aktualny wynik. Aby zmienić orientację ekranu, obróć urządzenie lub emulator. Zwróć uwagę, że bieżący wynik został utracony.
  3. Uruchom grę przez kilka słów. Gdy na ekranie pojawi się pewien wynik, zamknij i ponownie otwórz aplikację. Pamiętaj, że gra uruchamia się od początku, ponieważ stan aplikacji nie jest zapisany.
  4. Graj w kilka słów, a następnie kliknij przycisk Zakończ grę. Zwróć uwagę, że nic się nie dzieje.

Problemy z aplikacją:

  • Aplikacja startowa nie zapisuje i nie przywraca stanu aplikacji podczas zmiany konfiguracji, na przykład w przypadku zmiany orientacji urządzenia lub po wyłączeniu i ponownym uruchomieniu aplikacji.
    Możesz rozwiązać ten problem za pomocą wywołania zwrotnego onSaveInstanceState(). Użycie metody onSaveInstanceState() wymaga jednak napisania dodatkowego kodu, który pozwoli zapisać stan w pakiecie i wdrożyć logikę, by pobrać ten stan. Ponadto jest możliwość zminimalizowania ilości przechowywanych danych.
  • Gdy użytkownik kliknie przycisk Zakończ grę, nie przejdzie na ekran wyników.

Problemy te znajdziesz w komponentach architektury aplikacji, które omawiasz w ćwiczeniach z programowania.

Architektura aplikacji

Architektura aplikacji to sposób projektowania aplikacji, klas i relacji między nimi. Dzięki temu kod jest zorganizowany, działa dobrze w określonych sytuacjach i jest łatwy w pracy. W tym zestawie czterech ćwiczeń z programowania poprawki wprowadzone w aplikacji GuessTheWord są zgodne ze wskazówkami dotyczącymi architektury aplikacji na Androida i używasz komponentów architektury Androida. Architektura aplikacji na Androida jest podobna do wzorca MVVM (model-view-viewmodel).

Aplikacja GuessTheWord jest zgodna z zasadą oddzielania obaw i jest podzielona na klasy, a każda z nich zawiera osobne zagadnienia. W pierwszym ćwiczeniu z ćwiczeń, z którymi współpracujesz, będą kontrolery interfejsu, ViewModel i ViewModelFactory.

Kontroler interfejsu

Kontroler interfejsu to klasa interfejsu użytkownika, na przykład Activity lub Fragment. Kontroler interfejsu powinien zawierać wyłącznie logikę obsługującą interfejs użytkownika i interakcje z systemem operacyjnym, takie jak wyświetlanie widoków i przechwytywanie danych wejściowych użytkownika. Nie stosuj zasad decyzyjnych, takich jak logika decydująca o wyświetlaniu tekstu w kontrolerze interfejsu.

W kodzie początkowym GuessTheWord kontrolery interfejsu użytkownika składają się z 3 fragmentów: GameFragment, ScoreFragment, i TitleFragment. Zgodnie z zasadą „rozdzielania obaw” GameFragment ma za zadanie jedynie rysować elementy gry na ekranie i dowiedzieć się, kiedy użytkownik klika przyciski, a także wiele innych. Gdy użytkownik kliknie przycisk, te informacje zostaną przekazane do: GameViewModel.

Wyświetl model

ViewModel zawiera dane do wyświetlenia we fragmencie lub działaniu powiązanym z elementem ViewModel. ViewModel może wykonywać proste obliczenia i przekształcać dane, 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ż dane wyświetlają się na ekranie. GameViewModel zawiera też logikę biznesową, która pozwala przeprowadzić proste obliczenia, aby określić bieżący stan danych.

WidokModelFactory

ViewModelFactory tworzy instancje ViewModel z parametrami konstruktora lub bez nich.

W kolejnych ćwiczeniach z programowania poznasz inne komponenty architektury Androida, które są powiązane z kontrolerami interfejsu i usługą ViewModel.

Klasa ViewModel służy do przechowywania danych związanych z interfejsem użytkownika i zarządzania nimi. W tej aplikacji każdy element ViewModel jest powiązany z jednym fragmentem.

W tym zadaniu dodasz pierwszy ViewModel do swojej aplikacji GameViewModel na GameFragment. Dowiesz się też, co oznacza, że ViewModel jest zależny od cyklu życia.

Krok 1. Dodaj klasę GameViewModel

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

    Jeśli używasz najnowszej wersji biblioteki, aplikacja w zakresie rozwiązań powinna się skompilować zgodnie z oczekiwaniami. Jeśli tak się nie stanie, spróbuj rozwiązać problem lub przywróć podaną poniżej wersję.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. W folderze screens/game/ utwórz nową klasę Kotlin o nazwie GameViewModel.
  2. Ustaw jako klasę GameViewModel abstrakcyjną ViewModel.
  3. Aby ułatwić zrozumienie, w jaki sposób tag ViewModel jest zależny od cyklu życia, dodaj blok init za pomocą instrukcji log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Krok 2: Zastąp funkcję ClearClear() i dodaj logowanie

Element ViewModel jest niszczony po odłączeniu powiązanego fragmentu lub po zakończeniu działania. Tuż przed zniszczeniem elementu ViewModel wywoływane jest wywołanie zwrotne onCleared(), by wyczyścić zasoby.

  1. W klasie GameViewModel zastąp metodę onCleared().
  2. Dodaj instrukcję logowania w obrębie onCleared(), aby śledzić cykl życia elementu GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Krok 3. Powiąż GameGameModel z fragmentem z gry

Obiekt ViewModel musi być powiązany z kontrolerem interfejsu. Aby powiązać oba te elementy, utwórz odwołanie do ViewModel w kontrolerze interfejsu.

W tym kroku tworzysz odwołanie do GameViewModel w ramach odpowiedniego kontrolera 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 widok danych

Podczas zmiany konfiguracji, np. rotacji ekranu, kontrolery interfejsu (np. fragmenty) są tworzone ponownie. Instancje ViewModel działają jednak dalej. Jeśli utworzysz instancję ViewModel za pomocą klasy ViewModel, po każdym jej utworzeniu będzie tworzony nowy obiekt. Zamiast tego utwórz instancję ViewModel za pomocą ViewModelProvider.

Jak działa ViewModelProvider:

  • ViewModelProvider zwraca wartość ViewModel, jeśli już istnieje, lub tworzy nową, jeśli jeszcze nie istnieje.
  • ViewModelProvider tworzy instancję ViewModel powiązaną z określonym zakresem (aktywności lub fragmentu).
  • Utworzony ViewModel jest przechowywany, dopóki zakres jest aktywny. Jeśli na przykład zakres jest fragmentem, element ViewModel jest przechowywany, dopóki fragment nie zostanie odłączony.

Zainicjuj metodę ViewModel za pomocą metody ViewModelProviders.of(), aby utworzyć ViewModelProvider:

  1. W klasie GameFragment zainicjuj zmienną viewModel. Umieść ten kod w obrębie znaczników onCreateView() po definicji zmiennej wiązania. Użyj metody ViewModelProviders.of() i przekaż powiązany kontekst GameFragment i klasę GameViewModel.
  2. Nad inicjacją obiektu ViewModel dodaj instrukcję logu, by 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 użyj filtra Game. Kliknij przycisk Odtwórz na urządzeniu lub w emulatorze. Otworzy się ekran gry.

    Jak widać w LogCat, metoda onCreateView() metody GameFragment wywołuje metodę ViewModelProviders.of(), by utworzyć GameViewModel. Wyciągi dodane przez Ciebie do GameFragment i GameViewModel pojawią się w Logcat.

  1. Włącz ustawienie autoobracania w urządzeniu lub emulatorze i kilka razy zmień orientację ekranu. GameFragment jest niszczony i odtwarzany za każdym razem, więc wywołanie ViewModelProviders.of() jest wywoływane za każdym razem. GameViewModel jest tworzony tylko raz i nie jest tworzona ani niszczona 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. Wyjdź z gry lub pokaż jej fragment. GameFragment jest zniszczony. Powiązana jest też GameViewModel, a wywoływane jest wywołanie zwrotne onCleared().
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!

Plik ViewModel pokonuje zmiany konfiguracji, dlatego jest dobrym rozwiązaniem w przypadku danych, które muszą przetrwać zmiany konfiguracji:

  • Umieść dane do wyświetlania na ekranie i kod, aby je przetworzyć w narzędziu ViewModel.
  • Element ViewModel nie powinien nigdy zawierać odniesień do fragmentów, aktywności ani widoków danych, ponieważ aktywności, fragmenty i widoki danych nie przetrwają ze zmianą konfiguracji.

Dla porównania przyjrzyjmy się, jak dane GameFragment UI są obsługiwane w aplikacji startowej przed dodaniem ViewModel i po dodaniu ViewModel:

  • Zanim dodasz ViewModel:
    gdy aplikacja zmieni konfigurację (np. obrót ekranu), fragment gry zostanie zniszczony i odtworzony ponownie. Dane zostaną utracone.
  • Po dodaniu elementu ViewModel i przeniesieniu fragmentu interfejsu gry do interfejsu ViewModel:
    wszystkie dane, które mają być widoczne w tym fragmencie, to teraz ViewModel. Po wprowadzeniu zmiany w aplikacji ViewModel przetrwa, a dane zostaną zachowane.

W tym zadaniu przeniesiesz dane z interfejsu aplikacji do klasy GameViewModel wraz z metodami przetwarzania danych. W ten sposób dane zostaną zachowane podczas zmian konfiguracji.

Krok 1. Przenieś pola danych i przetwarzanie danych do elementu ModelModel

Przenieś następujące pola i metody danych z GameFragment do GameViewModel:

  1. Przenieś pola danych word, score i wordList. Upewnij się, że word i score to nie private.

    Nie przenoś zmiennej wiązania GameFragmentBinding, ponieważ zawiera odniesienia do widoków. Ta zmienna służy do zwiększania układu, konfigurowania detektorów kliknięć i wyświetlania danych na ekranie – obowiązków fragmentu.
  2. Przenieś metody resetList() i nextWord(). Te metody określają, jakie słowo ma być wyświetlane na ekranie.
  3. W ramach metody onCreateView() przenieś wywołania metody do resetList() i nextWord() do bloku init elementu GameViewModel.

    Te metody muszą znajdować się w bloku init, ponieważ lista słów powinna być resetowana podczas tworzenia ViewModel, a nie za każdym razem, gdy fragment jest tworzony. Możesz usunąć instrukcję dziennika w bloku init o wartości GameFragment.

Moduły obsługi kliknięć onSkip() i onCorrect() w GameFragment zawierają kod do przetwarzania danych i aktualizowania interfejsu użytkownika. Kod do aktualizowania interfejsu powinien pozostać we fragmencie, ale kod przetwarzania danych musi zostać przeniesiony do ViewModel.

Na razie umieść identyczne metody w obu miejscach:

  1. Skopiuj metody onSkip() i onCorrect() z GameFragment do GameViewModel.
  2. W tagu GameViewModel sprawdź, czy metody onSkip() i onCorrect() nie są private, ponieważ odwołasz się do tych metod z fragmentu.

Oto kod zajęć 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 modułów obsługi kliknięć i pól danych w GameFragment

  1. W GameFragment zaktualizuj metody onSkip() i onCorrect(). Usuń kod, aby zaktualizować wynik i wywołać odpowiednie metody onSkip() i onCorrect() w viewModel.
  2. Metoda nextWord() została przeniesiona do właściwości ViewModel, dlatego fragment kodu gry nie ma już do niego dostępu.

    W metodzie GameFragment w metodach onSkip() i onCorrect() zastąp wywołanie nextWord() elementami updateScoreText() i updateWordText(). Te metody wyświetlają dane na ekranie.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. W narzędziu GameFragment zaktualizuj zmienne score i word, by korzystały ze zmiennych GameViewModel, ponieważ zmienne znajdują się teraz w sekcji GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. W elemencie GameViewModel wewnątrz metody nextWord() usuń wywołania metody updateWordText() i updateScoreText(). Metody te są teraz wywoływane z elementu GameFragment.
  2. Utwórz aplikację i upewnij się, że nie ma w niej żadnych błędów. Jeśli wystąpiły błędy, wyczyść projekt i utwórz go od nowa.
  3. Uruchom aplikację i zagraj w grę za pomocą kilku słów. Będąc na ekranie gry, obróć urządzenie. Zwróć uwagę, że zmiana wyniku i bieżącego słowa zostaną zachowane.

Brawo! Teraz wszystkie dane aplikacji są przechowywane w narzędziu ViewModel, więc są przechowywane w trakcie zmian konfiguracji.

W tym zadaniu implementujesz detektor kliknięć przycisku Zakończ grę.

  1. W GameFragment dodaj metodę onEndGame(). Metoda onEndGame() zostanie wywołana, gdy użytkownik kliknie przycisk Zakończ grę.
private fun onEndGame() {
   }
  1. W metodzie GameFragment znajdź w metodzie onCreateView() kod, który ustawia odbiorniki dla przycisków OK i Pomiń. Pod tymi wierszami ustaw detektor kliknięć przycisku Zakończ grę. Użyj zmiennej wiązania binding. W odbiorniku kliknięć wywołaj metodę onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. W GameFragment dodaj metodę o nazwie gameFinished(), aby poruszać się po aplikacji na ekranie z wynikiem. Przekaż wynik jako argument za pomocą funkcji 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 przełączaj się między słowami. Kliknij przycisk Zakończ grę. Aplikacja wyświetli ekran z wynikiem, ale ostateczny wynik nie zostanie wyświetlony. W następnym zadaniu naprawisz to.

Gdy użytkownik zakończy grę, ScoreFragment nie wyświetla wyniku. Chcesz, aby ScoreFragment wyświetlał wynik w ViewModel. Podczas inicjowania funkcji ViewModel przekażesz wartość wyniku za pomocą wzorca metody fabrycznej.

Wzór metody fabrycznej to wzór konstrukcyjny, który wykorzystuje metody fabryczne do tworzenia obiektów. Metoda fabryczna to metoda zwracająca wystąpienie tej samej klasy.

W tym zadaniu tworzysz ViewModel z parametrowym konstruktorem dla fragmentu wyniku i metodą fabryczną, aby utworzyć instancję ViewModel.

  1. W pakiecie score utwórz nową klasę Kotlin o nazwie ScoreViewModel. Te fragmenty kodu będą zawierać ViewModel we fragmencie wyniku.
  2. Wydłuż klasę ScoreViewModel z ViewModel. Dodaj parametr konstruktora do ostatecznego wyniku. Dodaj blok init z deklaracją logu.
  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 odpowiadać za tworzenie instancji ScoreViewModel.
  2. Przedłuż klasę ScoreViewModelFactory z ViewModelProvider.Factory. Dodaj parametr konstruktora do wyniku końcowego.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. W ScoreViewModelFactory Studio Android wyświetla błąd o niezaimplementowanym elemencie abstrakcyjnym. Aby naprawić błąd, zastąp metodę create(). Metoda create() zwraca 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 klas ScoreViewModel i ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. W ScoreFragment w elemencie onCreateView() zainicjuj zmienną binding, inicjuj viewModelFactory. Użyj ScoreViewModelFactory. Przekaż końcowy wynik z pakietu argumentów jako parametr konstruktora do elementu ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. W onCreateView() po zainicjowaniu viewModelFactory zainicjuj obiekt viewModel. Wywołaj metodę ViewModelProviders.of(), przekaż kontekst powiązanego fragmentu wyniku i viewModelFactory. Spowoduje to utworzenie obiektu ScoreViewModel przy użyciu metody fabrycznej określonej w klasie viewModelFactory..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. W metodzie onCreateView() po zainicjowaniu viewModel ustaw tekst widoku scoreText na końcowy wynik zdefiniowany w ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Uruchom aplikację i zagraj w grę. Przełączaj się między słowami lub wszystkimi słowami i kliknij Zakończ grę. Zwróć uwagę, że fragment wyniku zawiera teraz ostateczny wynik.

  1. Opcjonalnie: sprawdź logi ScoreViewModel w narzędziu LogCat, filtrując je według wartości ScoreViewModel. Wartość wyniku powinna być wyświetlana.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

W tym zadaniu zaimplementowałeś ScoreFragment, aby używać ViewModel. Wiesz też, jak za pomocą interfejsu ViewModelFactory utworzyć konstruktor z parametrami dla ViewModel.

Gratulacje! Zmieniłeś architekturę aplikacji, aby używać jednego z komponentów architektury Androida: ViewModel. Rozwiązano problem z cyklem życia aplikacji i teraz dane pozostają w stanie po zmianie konfiguracji. Wiesz też, jak utworzyć konstruktor z parametrami do tworzenia obiektu ViewModel za pomocą interfejsu ViewModelFactory.

Projekt na Android Studio: GuessTheWord

  • Wytyczne dotyczące architektury aplikacji na Androida zaleca się oddzielać zajęcia, które pełnią różne obowiązki.
  • Kontroler interfejsu to klasa interfejsu, na przykład Activity lub Fragment. Kontrolery interfejsu użytkownika powinny zawierać wyłącznie funkcje umożliwiające obsługę interakcji użytkownika i systemu operacyjnego. Nie powinny zawierać danych do wyświetlania w interfejsie. Umieść te dane w narzędziu ViewModel.
  • Klasa ViewModel przechowuje dane związane z interfejsem użytkownika i nimi zarządza. Klasa ViewModel umożliwia przetrwanie zmian konfiguracji, takich jak obrót ekranu.
  • ViewModel to jeden z zalecanych komponentów architektury Androida.
  • ViewModelProvider.Factory to interfejs, którego można użyć do utworzenia obiektu ViewModel.

Tabela poniżej zawiera porównanie kontrolerów interfejsu z instancjami ViewModel, które zawierają dla nich dane:

Kontroler interfejsu

WidokModel

Przykładowym kontrolerem interfejsu jest ScoreFragment utworzony w tym ćwiczeniu z programowania.

Przykład: ViewModel to ScoreViewModel utworzony w tym ćwiczeniu z programowania.

Nie zawiera żadnych danych do wyświetlenia w interfejsie użytkownika.

Zawiera dane, które kontroler interfejsu wyświetla w interfejsie.

Zawiera kod do wyświetlania danych oraz kod zdarzenia użytkownika, np. detektor kliknięć.

Zawiera kod do przetwarzania danych.

Zniszczona i odtwarzana podczas każdej zmiany konfiguracji.

Jest zniszczone tylko wtedy, gdy powiązany kontroler interfejsu zniknie na stałe – w przypadku aktywności, po zakończeniu aktywności lub w przypadku jej odłączenia.

Zawiera widoki.

Element nie powinien nigdy zawierać odwołań do aktywności, fragmentów ani widoków danych, ponieważ nie przetrwają one ze zmianą konfiguracji, ale tag ViewModel je ma.

Zawiera odwołanie do powiązanego elementu ViewModel.

Nie zawiera żadnego odniesienia do powiązanego kontrolera interfejsu.

Kurs Udacity:

Dokumentacja dla programistów Androida:

Inne:

Ta sekcja zawiera listę możliwych zadań domowych dla uczniów, którzy pracują w ramach tego ćwiczenia w ramach kursu prowadzonego przez nauczyciela. To nauczyciel może wykonać te czynności:

  • W razie potrzeby przypisz zadanie domowe.
  • Poinformuj uczniów, jak przesyłać zadania domowe.
  • Oceń projekty domowe.

Nauczyciele mogą wykorzystać te sugestie tak długo, jak chcą lub chcą, i mogą przypisać dowolne zadanie domowe.

Jeśli samodzielnie wykonujesz te ćwiczenia z programowania, możesz sprawdzić swoją wiedzę w tych zadaniach domowych.

Odpowiedz na te pytania

Pytanie 1

Wybierz klasę, aby zapisać dane aplikacji. Pozwoli to uniknąć utraty danych podczas zmiany konfiguracji urządzenia.

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Pytanie 2

Element ViewModel nie powinien nigdy zawierać żadnych odniesień do fragmentów, aktywności ani widoków danych. Prawda czy fałsz?

  • Prawda
  • Fałsz

Pytanie 3

Kiedy nastąpi ViewModel?

  • Gdy powiązany kontroler interfejsu użytkownika zostanie zniszczony i odtworzony ponownie w wyniku zmiany orientacji urządzenia.
  • w zmianie orientacji.
  • Po zakończeniu działania powiązanego kontrolera (jeśli jest to działanie) lub odłączonym (jeśli jest to fragment).
  • Gdy użytkownik naciśnie przycisk Wstecz.

Pytanie 4

Do czego służy interfejs ViewModelFactory?

  • Tworzę instancję obiektu ViewModel.
  • Przechowywanie danych podczas zmiany orientacji.
  • Odświeżam dane wyświetlane na ekranie.
  • otrzymywanie powiadomień o zmianie danych aplikacji.

Rozpocznij następną lekcję: 5.2: obserwatorzy LiveData i LiveData

Linki do innych ćwiczeń z programowania w tym kursie znajdziesz na stronie docelowej z ćwiczeniami z podstaw Androida Kotlin.