Android Kotlin Fundamentals 05.3: wiązanie danych z obiektami ViewModel i LiveData

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.

Wstęp

Podczas poprzednich lekcji udało Ci się ulepszyć kod aplikacji GuessTheWord. Aplikacja korzysta teraz z obiektów ViewModel, więc dane pozostają w stanie po zmianie konfiguracji urządzenia, np. o obróceniu ekranu i zmianie dostępności klawiatury. Dodano też możliwość obserwowania okresu LiveData, dzięki czemu powiadomienia są automatycznie powiadamiane o zmianach.

W tym ćwiczeniu z programowania będziesz pracować z aplikacją GuessTheWord. Powiążesz widoki z klasami ViewModel w aplikacji, aby widoki w Twoim układzie komunikowały się bezpośrednio z obiektami ViewModel. (Do tej pory wyświetlenia w aplikacji przekazywały pośrednio informacje od ViewModel z użyciem fragmentów aplikacji). Po zintegrowaniu wiązania danych z obiektami ViewModel nie potrzebujesz już modułów obsługi kliknięć we fragmentach kodu aplikacji, więc je usuniesz.

Możesz też zmodyfikować aplikację GuesTheWord jako źródło wiążących danych i powiadomić użytkownika o zmianach w danych bez korzystania z metod LiveData.

Co musisz wiedzieć

  • Jak tworzyć podstawowe aplikacje na Androida w Kotlinie.
  • Jak działają cykle życia aktywności i fragmentów.
  • Jak używać obiektów ViewModel w aplikacji.
  • Jak przechowywać dane za pomocą LiveData w narzędziu ViewModel.
  • Jak dodać metody obserwacji, aby obserwować zmiany w danych LiveData.

Czego się nauczysz

  • Jak używać elementów z Biblioteki wiązania danych
  • Jak zintegrować ViewModel z wiązaniem danych.
  • Jak zintegrować LiveData z wiązaniem danych.
  • Jak używać powiązań słuchaczy, by zastąpić detektory kliknięć we fragmencie.
  • Jak dodać formatowanie ciągu do wyrażeń wiążących dane.

Jakie zadania wykonasz:

  • Widoki w układach GuessTheWord komunikują się pośrednio z obiektami ViewModel, korzystając z kontrolerów (fragmentów) interfejsu użytkownika, aby przekazywać informacje. W ramach tego ćwiczenia powiążesz widoki aplikacji z obiektami ViewModel, tak by widoki komunikowania się bezpośrednio z obiektami ViewModel.
  • Zmieniasz aplikację, aby używać źródła danych jako LiveData. Po tej zmianie obiekty LiveData powiadamiają interfejs użytkownika o zmianach w danych, a metody obserwatora LiveData nie są już potrzebne.

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).

Dzięki nim dowiesz się, jak ulepszyć aplikację GuessTheWord, integrując wiązanie danych z obiektem LiveData w obiektach ViewModel. Automatyzuje ona komunikację między widokami w układzie i obiektami ViewModel, a także upraszcza korzystanie z kodu za pomocą metody LiveData.

Ekran tytułowy

Ekran gry

Ekran wyniku

W tym zadaniu znajdziesz i uruchomisz kod startowy tego ćwiczenia z programowania. Możesz użyć aplikacji GuessTheWord, która została opracowana w ramach poprzedniego ćwiczenia z programowania, lub pobrać aplikację inicjowaną.

  1. (Opcjonalnie) Jeśli nie używasz kodu z poprzedniego ćwiczenia z programowaniem, pobierz kod startowy tego ćwiczenia. Rozpakuj kod i otwórz projekt w Android Studio.
  2. Uruchom aplikację i zagraj w grę.
  3. Przycisk OK wyświetla następne słowo i podnosi wynik o jeden, a przycisk Pomiń pokazuje kolejne słowo i zmniejsza wynik o 1. Przycisk Zakończ grę kończy grę.
  4. Przełączaj się między słowami i zauważ, że aplikacja automatycznie przechodzi do ekranu z wynikiem.

W poprzednim ćwiczeniu z programowania zdarzyło Ci się wiązać dane w sposób zapewniający dostęp do widoków w aplikacji GuessTheWord. Prawdziwą zaletą wiązania danych jest to, że sugerujemy powiązanie danych bezpośrednio z widokami aplikacji.

Aktualna architektura aplikacji

W aplikacji widoki są zdefiniowane w układzie XML, a dane dla tych widoków są przechowywane w obiektach ViewModel. Każdy widok danych i odpowiadający mu element ViewModel to kontroler interfejsu, który pełni rolę przekaźnika.

Przykład:

  • Przycisk OK jest zdefiniowany jako widok Button w pliku układu game_fragment.xml.
  • Gdy użytkownik kliknie przycisk Rozumiem, detektor kliknięć we fragmencie GameFragment wywoła odpowiedni moduł nasłuchujący kliknięć w aplikacji GameViewModel.
  • Wynik zostanie zaktualizowany w GameViewModel.

Widok danych Button i GameViewModel nie komunikują się bezpośrednio – potrzebują detektora kliknięć podanego w GameFragment.

ViewModel przekazany do powiązania danych

Będzie łatwiej, jeśli widoki w układzie komunikują się bezpośrednio z danymi w obiektach ViewModel bez konieczności korzystania z kontrolerów interfejsu jako pośredników.

Obiekty ViewModel zawierają wszystkie dane interfejsu użytkownika dostępne w aplikacji GuessTheWord. Przekazując obiekty ViewModel do powiązania danych, możesz zautomatyzować komunikację między obiektami a obiektami ViewModel.

W tym zadaniu kojarzysz klasy GameViewModel i ScoreViewModel z odpowiadającymi im układami XML. Konfigurujesz też powiązania słuchacza z danymi o kliknięciach.

Krok 1. Dodaj wiązanie danych do GameViewModel

W tym kroku powiążesz element GameViewModel z odpowiednim plikiem układu game_fragment.xml.

  1. W pliku game_fragment.xml dodaj zmienną wiązania danych typu GameViewModel. Jeśli w Android Studio występują błędy, wyczyść projekt i utwórz go od nowa.
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. W pliku GameFragment przekaż GameViewModel do wiązania danych.

    W tym celu przypisz element viewModel do zmiennej binding.gameViewModel zadeklarowanej w poprzednim kroku. Umieść ten kod w sekcji onCreateView() po inicjowaniu elementu viewModel. Jeśli w Android Studio występują błędy, wyczyść projekt i utwórz go od nowa.
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

Krok 2. Używaj powiązań detektora do obsługi zdarzeń

Wiązania detektorów to wyrażenia wiążące, które są uruchamiane po wywołaniu zdarzeń takich jak onClick(), onZoomIn() lub onZoomOut(). Wiązania detektorów są zapisywane jako wyrażenia lambda.

Wiązanie danych tworzy detektor i ustawia detektor w widoku. Kiedy wystąpi nasłuchiwanie zdarzenia, detektor ocenia wyrażenie lambda. Wiązania słuchaczy działają z wtyczką Androida do obsługi Gradle w wersji 2.0 lub nowszej. Więcej informacji znajdziesz w artykule Układy i wyrażenia powiązania.

W tym kroku zastąpisz detektory kliknięć w pliku GameFragment powiązaniami detektorów w pliku game_fragment.xml.

  1. W game_fragment.xml dodaj atrybut onClick do skip_button. Zdefiniuj wyrażenie powiązania i wywołaj metodę onSkip() w GameViewModel. Jest to tzw. powiązanie słuchacza.
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. Podobnie powiąż zdarzenie kliknięcia elementu correct_button z metodą onCorrect() w narzędziu GameViewModel.
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. Powiąż zdarzenie kliknięcia elementu end_game_button z metodą onGameFinish() w narzędziu GameViewModel.
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. W usłudze GameFragment usuń instrukcje, które ustawiają detektory kliknięć, i usuń funkcje wywoływane przez detektory kliknięć. Nie są Ci już potrzebne.

Kod do usunięcia:

binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }

/** Methods for buttons presses **/
private fun onSkip() {
   viewModel.onSkip()
}
private fun onCorrect() {
   viewModel.onCorrect()
}
private fun onEndGame() {
   gameFinished()
}

Krok 3. Dodaj wiązanie danych do obiektu ScoreViewModel

W tym kroku powiążesz element ScoreViewModel z odpowiednim plikiem układu score_fragment.xml.

  1. W pliku score_fragment.xml dodaj zmienną wiązania typu ScoreViewModel. Ten krok jest podobny do wykonanego przez Ciebie powyżej dla kroku GameViewModel.
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. W score_fragment.xml dodaj atrybut onClick do play_again_button. Zdefiniuj powiązanie detektora i wywołaj metodę onPlayAgain() w ScoreViewModel.
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. W narzędziu ScoreFragment wewnątrz onCreateView() zainicjuj viewModel. Następnie zainicjuj zmienną powiązania binding.scoreViewModel.
viewModel = ...
binding.scoreViewModel = viewModel
  1. W kodzie ScoreFragment usuń kod, który ustawia detektor kliknięć playAgainButton. Jeśli w Android Studio wyświetla się błąd, wyczyść projekt i utwórz go od nowa.

Kod do usunięcia:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Uruchom swoją aplikację. Aplikacja powinna działać tak jak wcześniej, ale teraz widoki przycisku automatycznie się komunikują z obiektami ViewModel. Widoki nie mogą już komunikować się za pomocą modułów obsługi kliknięć przycisku w ScoreFragment.

Rozwiązywanie problemów związanych z komunikatami o błędach wiązania danych

Gdy aplikacja korzysta z powiązania danych, proces kompilacji generuje klasy pośrednie używane do wiązania danych. Aplikacja może zawierać błędy, których Android Studio nie wykryje do czasu, gdy skompilujesz aplikację, więc podczas jej tworzenia nie będziesz widzieć ostrzeżeń ani czerwonego kodu. Jednak w momencie kompilacji pojawiają się błędy tajne pochodzące z wygenerowanych klas pośrednich.

Jeśli zobaczysz zagadkowy komunikat o błędzie:

  1. Przeczytaj komunikat w panelu kompilacji Android Studio. Jeśli zobaczysz lokalizację, której nazwa kończy się databinding, wystąpił błąd powiązania danych.
  2. Sprawdź, czy w pliku XML układu nie występują błędy w atrybutach onClick wiążących dane. Znajdź funkcję wywołującą wyrażenie lambda i upewnij się, że istnieje.
  3. W sekcji <data> pliku XML sprawdź pisownię zmiennej wiązania danych.

Zwróć uwagę na przykład na błąd w nazwie funkcji onCorrect() w tej wartości atrybutu:

android:onClick="@{() -> gameViewModel.onCorrectx()}"

Sprawdź też pisownię błędu gameViewModel w sekcji <data> pliku XML:

<data>
   <variable
       name="gameViewModelx"
       type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>

Android Studio nie wykrywa takich błędów, dopóki nie skompilujesz aplikacji, a kompilator wyświetli komunikat o błędzie podobny do tego:

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding

Wiązanie danych dobrze działa z elementami LiveData, które są używane z obiektami ViewModel. Po dodaniu powiązania danych do obiektów ViewModel możesz zacząć stosować LiveData.

W tym zadaniu zmienisz aplikację GuessTheWord tak, aby wykorzystywała LiveData jako źródło wiązania danych do powiadamiania interfejsu użytkownika o zmianach w danych bez korzystania z metod obserwacji LiveData.

Krok 1. Dodaj słowo LiveData do pliku game_fragment.xml

W tym kroku powiążesz bieżący widok tekstu słowa bezpośrednio z obiektem LiveData w elemencie ViewModel.

  1. W game_fragment.xml dodaj atrybut android:text do widoku tekstu word_text.

Ustaw jako obiekt LiveData obiekt word z GameViewModel za pomocą zmiennej wiązania gameViewModel.

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

Uwaga: nie musisz używać atrybutu word.value. Zamiast niego możesz użyć rzeczywistego obiektu LiveData. Obiekt LiveData wyświetla bieżącą wartość elementu word. Jeśli wartość word ma wartość null, obiekt LiveData wyświetla pusty ciąg znaków.

  1. W GameFragment w narzędziu onCreateView() po zainicjowaniu gameViewModel ustaw bieżącą aktywność jako właściciel cyklu życia zmiennej binding. Określa zakres obiektu LiveData powyżej, umożliwiając automatyczne aktualizowanie widoków w układzie game_fragment.xml.
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. W GameFragment usuń obserwatora dla elementu LiveData word.

Kod do usunięcia:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. Uruchom aplikację i zagraj w grę. Bieżące słowo jest aktualizowane w interfejsie bez metody obserwatora.

Krok 2. Dodaj wynik LiveData do pliku score_fragment.xml

W tym kroku powiążesz element LiveData score z widokiem tekstu wyniku we fragmencie wyniku.

  1. W score_fragment.xml dodaj atrybut android:text do widoku tekstu wyniku. Przypisz scoreViewModel.score do atrybutu text. Parametr score jest liczbą całkowitą, dlatego należy przekonwertować go na ciąg znaków za pomocą funkcji String.valueOf().
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. W ScoreFragment po zainicjowaniu scoreViewModel ustaw bieżącą aktywność jako właściciela cyklu życia zmiennej binding.
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. W ScoreFragment usuń obserwatora obiektu score.

Kod do usunięcia:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Uruchom aplikację i zagraj w grę. Zwróć uwagę, że wynik w fragmencie wyniku jest wyświetlany prawidłowo, bez obserwatora we fragmencie wyniku.

Krok 3. Dodaj formatowanie ciągu znaków przy użyciu wiązania danych

W układzie możesz dodać formatowanie ciągu razem z wiązaniem danych. W tym zadaniu formatujesz obecne słowo, aby dodać w nim cudzysłów. Formatujesz też wynik, dodając do niego bieżący wynik, jak pokazano na poniższym obrazie.

  1. W string.xml dodaj te ciągi znaków, które będą służyć do formatowania widoków tekstowych word i score. %s i %d są obiektami zastępczymi bieżącego słowa i aktualnego wyniku.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. W narzędziu game_fragment.xml zaktualizuj atrybut text widoku tekstowego word_text, aby używał zasobu ciągu znaków quote_format. Przejdź za gameViewModel.word. Przekazuje ono aktualne słowo jako argument do ciągu formatowania.
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. Formatuj widok tekstowy score podobny do word_text. W game_fragment.xml dodaj atrybut text do widoku tekstu score_text. Użyj zasobu ciągu znaków score_format, który przyjmuje 1 argument liczbowy reprezentowany przez symbol zastępczy %d. Jako argument tego ciągu formatowania przekaż obiekt LiveData (score).
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. W klasie GameFragment usuń kod obserwatora z metody score.

Kod do usunięcia:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Wyczyść, odtwórz i uruchom aplikację, a potem zagraj w grę. Zwróć uwagę, że bieżące słowo i wynik zostaną sformatowane na ekranie gry.

Gratulacje! Zintegrowano LiveData i ViewModel z wiązaniem danych w aplikacji. Umożliwi to bezpośrednie komunikowanie się z elementami ViewModel w układzie z wykorzystaniem modułów obsługi kliknięć we fragmencie. Obiekty LiveData zostały też wykorzystane jako źródło wiązania danych do automatycznego powiadamiania interfejsu użytkownika o zmianach w danych, bez korzystania z metod obserwacji LiveData.

Projekt na Android Studio: GuessTheWord

  • Biblioteka wiązania danych działa bezproblemowo z komponentami Android Architecture, takimi jak ViewModel i LiveData.
  • Układy w aplikacji mogą tworzyć powiązanie z danymi w komponentach architektury, które ułatwiają zarządzanie cyklem życia interfejsu użytkownika i powiadamianie o zmianach w danych.

Wiązanie danych ViewModel

  • Możesz powiązać obiekt ViewModel z układem, używając wiązania danych.
  • Obiekty ViewModel zawierają dane interfejsu użytkownika. Przekażąc obiekty ViewModel do powiązania danych, możesz zautomatyzować komunikację między widokami a obiektami ViewModel.

Jak powiązać element ViewModel z układem:

  • W pliku układu dodaj zmienną wiązania danych typu ViewModel.
   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • W pliku GameFragment przekaż GameViewModel do wiązania danych.
binding.gameViewModel = viewModel

Powiązania detektora

  • Wiązania detektorów to wyrażenia w układzie, które są uruchamiane po wywołaniu zdarzeń kliknięcia, takich jak onClick().
  • Wiązania detektorów są zapisywane jako wyrażenia lambda.
  • Za pomocą powiązań słuchaczy możesz zastąpić detektory kliknięć w kontrolerach interfejsu wiązaniami detektora w pliku układu.
  • Wiązanie danych tworzy detektor i ustawia detektor w widoku.
 android:onClick="@{() -> gameViewModel.onSkip()}"

Dodawanie LiveData do wiązania danych

  • Obiekty LiveData mogą służyć jako źródło wiązania danych, aby automatycznie powiadamiać interfejs użytkownika o zmianach w danych.
  • Możesz powiązać ten widok bezpośrednio z obiektem LiveData w narzędziu ViewModel. Gdy LiveData w narzędziu ViewModel ulegnie zmianie, widoki w układzie mogą być aktualizowane automatycznie bez użycia metod obserwacji w kontrolerach interfejsu.
android:text="@{gameViewModel.word}"
  • Aby powiązanie danych LiveData działało, ustaw obecną aktywność (kontroler interfejsu) jako właściciela cyklu życia zmiennej binding w kontrolerze interfejsu.
binding.lifecycleOwner = this

Formatowanie ciągów z wiązaniem danych

  • Dzięki wiązaniu danych możesz sformatować zasób ciągu tekstowego za pomocą symboli zastępczych, takich jak %s w przypadku ciągów tekstowych i %d w przypadku liczb całkowitych.
  • Aby zaktualizować atrybut text widoku, przekaż obiekt LiveData jako argument ciągu tekstowego.
 android:text="@{@string/quote_format(gameViewModel.word)}"

Kurs Udacity:

Dokumentacja dla programistów Androida:

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

Które z poniższych stwierdzeń na temat powiązań słuchaczy nie jest prawdziwe?

  • Powiązania detektora to wyrażenia powiązania, które są uruchamiane, gdy wystąpi zdarzenie.
  • Powiązania detektora działają ze wszystkimi wersjami wtyczki Androida do obsługi Gradle.
  • Wiązania detektorów są zapisywane jako wyrażenia lambda.
  • Wiązania detektorów są podobne do odniesień do metod, ale pozwalają uruchamiać dowolne wyrażenia wiążące dane.

Pytanie 2

Załóżmy, że Twoja aplikacja zawiera ten zasób ciągu znaków:
<string name="generic_name">Hello %s</string>

Która z tych odpowiedzi jest prawidłowa, aby sformatować ciąg znaków przy użyciu wyrażenia wiążącego dane?

  • android:text= "@{@string/generic_name(user.name)}"
  • android:text= "@{string/generic_name(user.name)}"
  • android:text= "@{@generic_name(user.name)}"
  • android:text= "@{@string/generic_name,user.name}"

Pytanie 3

Kiedy następuje ocenianie i powiązanie detektora?

  • Po zmianie danych przechowywanych w LiveData
  • Gdy aktywność zostanie ponownie utworzona przez zmianę konfiguracji
  • Kiedy wystąpi zdarzenie takie jak onClick()
  • Gdy aktywność zaczyna działać w tle

Rozpocznij następną lekcję: 5.4 – przekształcenia LiveData

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