Основы Android Kotlin 05.3: Привязка данных с помощью ViewModel и LiveData

Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .

Введение

В предыдущих практических работах этого урока вы улучшили код приложения GuessTheWord. Теперь приложение использует объекты ViewModel , поэтому данные приложения сохраняются при изменении конфигурации устройства, например, при повороте экрана и изменении доступности клавиатуры. Вы также добавили наблюдаемые LiveData , поэтому представления автоматически уведомляются об изменении наблюдаемых данных.

В этой лабораторной работе вы продолжите работу с приложением GuessTheWord. Вы привяжете представления к классам ViewModel в приложении, чтобы представления в вашем макете напрямую взаимодействовали с объектами ViewModel . (До сих пор представления в вашем приложении взаимодействовали с ViewModel косвенно , через фрагменты приложения.) После интеграции привязки данных к объектам ViewModel обработчики щелчков во фрагментах приложения больше не нужны, поэтому вы их удаляете.

Вы также изменяете приложение GuessTheWord так, чтобы оно использовало LiveData в качестве источника привязки данных для уведомления пользовательского интерфейса об изменениях в данных без использования методов наблюдателя LiveData .

Что вам уже следует знать

  • Как создавать простые приложения для Android на Kotlin.
  • Как работают жизненные циклы активностей и фрагментов.
  • Как использовать объекты ViewModel в вашем приложении.
  • Как хранить данные с помощью LiveData в ViewModel .
  • Как добавить методы наблюдателя для наблюдения за изменениями в данных LiveData .

Чему вы научитесь

  • Как использовать элементы библиотеки привязки данных .
  • Как интегрировать ViewModel с привязкой данных.
  • Как интегрировать LiveData с привязкой данных.
  • Как использовать привязки прослушивателей для замены прослушивателей щелчков во фрагменте.
  • Как добавить форматирование строк в выражения привязки данных.

Что ты будешь делать?

  • Представления в макетах GuessTheWord взаимодействуют с объектами ViewModel косвенно, используя контроллеры пользовательского интерфейса (фрагменты) для передачи информации. В этой лабораторной работе вы свяжете представления приложения с объектами ViewModel , чтобы эти представления взаимодействовали напрямую с объектами ViewModel .
  • Вы изменяете приложение так, чтобы оно использовало LiveData в качестве источника привязки данных. После этого изменения объекты LiveData уведомляют пользовательский интерфейс об изменениях данных, и методы наблюдателя LiveData больше не нужны.

В практических занятиях по коду Урока 5 вы разработаете приложение GuessTheWord, начав с базового кода. GuessTheWord — это игра в стиле шарады для двух игроков, где игроки объединяют усилия, чтобы набрать как можно больше очков.

Первый игрок смотрит на слова в приложении и по очереди разыгрывает каждое слово, не показывая его второму игроку. Второй игрок пытается угадать слово.

Чтобы начать игру, первый игрок открывает приложение на устройстве и видит слово, например, «гитара», как показано на снимке экрана ниже.

Первый игрок изображает слово, стараясь не произносить его целиком.

  • Когда второй игрок правильно отгадывает слово, первый игрок нажимает кнопку « Понял» , что увеличивает счет на единицу и показывает следующее слово.
  • Если второй игрок не может угадать слово, первый игрок нажимает кнопку «Пропустить» , что уменьшает счет на единицу и переходит к следующему слову.
  • Чтобы завершить игру, нажмите кнопку «Завершить игру» . (Эта функция отсутствует в стартовом коде первой лабораторной работы в серии.)

В этой лабораторной работе вы улучшите приложение GuessTheWord, интегрировав привязку данных к LiveData в объектах ViewModel . Это автоматизирует взаимодействие между представлениями в макете и объектами ViewModel и позволяет упростить код за счёт использования LiveData .

Титульный экран

Игровой экран

Экран результатов

В этом задании вам предстоит найти и запустить стартовый код для этой лабораторной работы. В качестве стартового кода вы можете использовать приложение GuessTheWord, созданное в предыдущей лабораторной работе, или скачать готовое стартовое приложение.

  1. (Необязательно) Если вы не используете код из предыдущей лабораторной работы, скачайте начальный код для этой. Распакуйте код и откройте проект в Android Studio.
  2. Запустите приложение и играйте в игру.
  3. Обратите внимание, что кнопка «Понял» показывает следующее слово и увеличивает счёт на один, а кнопка «Пропустить» показывает следующее слово и уменьшает счёт на один. Кнопка «Завершить игру» завершает игру.
  4. Просмотрите все слова и обратите внимание, что приложение автоматически переходит на экран с очками.

В предыдущей лабораторной работе вы использовали привязку данных как типобезопасный способ доступа к представлениям в приложении GuessTheWord. Но настоящая сила привязки данных заключается в том, что она делает то, что следует из названия: привязывает данные непосредственно к объектам представлений в вашем приложении.

Текущая архитектура приложения

В вашем приложении представления определяются в XML-макете, а данные для них хранятся в объектах ViewModel . Между каждым представлением и соответствующей ему ViewModel находится контроллер пользовательского интерфейса, который выступает в роли связующего звена между ними.

Например:

  • Кнопка «Понял» определена как представление Button в файле макета game_fragment.xml .
  • Когда пользователь нажимает кнопку «Понял» , прослушиватель щелчков во фрагменте GameFragment вызывает соответствующий прослушиватель щелчков в GameViewModel .
  • Счет обновляется в GameViewModel .

Представление Button и GameViewModel не взаимодействуют напрямую — им нужен прослушиватель щелчков, который находится в GameFragment .

ViewModel передается в привязку данных

Было бы проще, если бы представления в макете напрямую взаимодействовали с данными в объектах ViewModel , не полагаясь на контроллеры пользовательского интерфейса в качестве посредников.

Объекты ViewModel содержат все данные пользовательского интерфейса в приложении GuessTheWord. Передавая объекты ViewModel в привязку данных, можно автоматизировать часть взаимодействия между представлениями и объектами ViewModel .

В этой задаче вы свяжете классы GameViewModel и ScoreViewModel с соответствующими им XML-макетами. Вы также настроите привязки прослушивателей для обработки событий щелчков.

Шаг 1: Добавьте привязку данных для GameViewModel

На этом этапе вы связываете GameViewModel с соответствующим файлом макета game_fragment.xml .

  1. В файле game_fragment.xml добавьте переменную привязки данных типа GameViewModel . Если в Android Studio возникли ошибки, исправьте их и пересоберите проект.
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. В файле GameFragment передайте GameViewModel в привязку данных.

    Для этого присвойте viewModel переменной binding.gameViewModel , объявленной на предыдущем шаге. Вставьте этот код в onCreateView() после инициализации viewModel . Если в Android Studio возникли ошибки, исправьте их и пересоберите проект.
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

Шаг 2: Используйте привязки прослушивателя для обработки событий

Привязки прослушивателя — это выражения привязки, которые выполняются при возникновении таких событий, как onClick() , onZoomIn() или onZoomOut() . Привязки прослушивателя записываются в виде лямбда-выражений.

Привязка данных создаёт прослушиватель и устанавливает его в представлении. При возникновении ожидаемого события прослушиватель вычисляет лямбда-выражение. Привязки прослушивателей работают с плагином Android Gradle версии 2.0 и выше. Подробнее см. в разделе Макеты и выражения привязки .

На этом этапе вы заменяете прослушиватели щелчков в GameFragment привязками прослушивателей в файле game_fragment.xml .

  1. В game_fragment.xml добавьте атрибут onClick к skip_button . Определите выражение привязки и вызовите метод onSkip() в GameViewModel . Это выражение привязки называется привязкой слушателя .
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. Аналогично привяжите событие нажатия correct_button к методу onCorrect () в GameViewModel .
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. Свяжите событие нажатия кнопки end_game_button с методом onGameFinish () в GameViewModel .
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. В GameFragment удалите операторы, устанавливающие обработчики щелчков, и удалите функции, которые они вызывают. Они вам больше не понадобятся.

Код для удаления:

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()
}

Шаг 3: Добавьте привязку данных для ScoreViewModel

На этом этапе вы связываете ScoreViewModel с соответствующим файлом макета, score_fragment.xml .

  1. В файле score_fragment.xml добавьте переменную привязки типа ScoreViewModel . Этот шаг аналогичен тому, что вы сделали для GameViewModel выше.
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. В score_fragment.xml добавьте атрибут onClick к play_again_button . Определите привязку прослушивателя и вызовите метод onPlayAgain() в ScoreViewModel .
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. В ScoreFragment , внутри onCreateView() , инициализируйте viewModel . Затем инициализируйте переменную привязки binding.scoreViewModel .
viewModel = ...
binding.scoreViewModel = viewModel
  1. В ScoreFragment удалите код, который устанавливает прослушиватель кликов для playAgainButton . Если Android Studio выдаёт ошибку, очистите и пересоберите проект.

Код для удаления:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Запустите приложение. Оно должно работать как и раньше, но теперь представления кнопок взаимодействуют напрямую с объектами ViewModel . Представления больше не взаимодействуют через обработчики нажатий кнопок в ScoreFragment .

Устранение неполадок с сообщениями об ошибках привязки данных

Когда приложение использует привязку данных, процесс компиляции генерирует промежуточные классы, которые используются для этой привязки. Приложение может содержать ошибки, которые Android Studio не обнаруживает, пока вы не попытаетесь скомпилировать приложение, поэтому вы не видите предупреждений или красного кода во время написания кода. Однако во время компиляции вы получаете непонятные ошибки, связанные с сгенерированными промежуточными классами.

Если вы получили непонятное сообщение об ошибке:

  1. Внимательно посмотрите на сообщение на панели сборки Android Studio. Если вы видите расположение, заканчивающееся на databinding , это означает, что произошла ошибка привязки данных.
  2. В XML-файле макета проверьте наличие ошибок в атрибутах onClick , использующих привязку данных. Найдите функцию, вызываемую лямбда-выражением, и убедитесь, что она существует.
  3. В разделе <data> XML проверьте правильность написания переменной привязки данных.

Например, обратите внимание на неправильное написание имени функции onCorrect() в следующем значении атрибута:

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

Также обратите внимание на опечатку в написании gameViewModel в разделе <data> XML-файла:

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

Android Studio не обнаруживает подобные ошибки, пока приложение не скомпилируется, после чего компилятор выводит сообщение об ошибке, подобное следующему:

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

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

Привязка данных хорошо работает с LiveData , используемой с объектами ViewModel . Теперь, когда вы добавили привязку данных к объектам ViewModel , вы готовы к интеграции LiveData .

В этой задаче вы измените приложение GuessTheWord так, чтобы оно использовало LiveData в качестве источника привязки данных для уведомления пользовательского интерфейса об изменениях в данных, не прибегая к методам наблюдателя LiveData .

Шаг 1: Добавьте слово LiveData в файл game_fragment.xml.

На этом этапе вы привязываете текущее текстовое представление слова непосредственно к объекту LiveData в ViewModel .

  1. В game_fragment.xml добавьте атрибут android:text к текстовому представлению word_text .

Установите его для объекта LiveData , word из GameViewModel , используя переменную привязки gameViewModel .

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

Обратите внимание, что вам не обязательно использовать word.value . Вместо этого вы можете использовать сам объект LiveData . Объект LiveData отображает текущее значение word . Если значение word равно null, объект LiveData отображает пустую строку.

  1. В GameFragment , в onCreateView() , после инициализации gameViewModel , установите текущую активность в качестве владельца жизненного цикла переменной binding . Это определяет область действия объекта LiveData , указанную выше, позволяя объекту автоматически обновлять представления в макете 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. В GameFragment удалите наблюдателя для word LiveData .

Код для удаления:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. Запустите приложение и играйте. Теперь текущее слово обновляется без метода-наблюдателя в контроллере пользовательского интерфейса.

Шаг 2: Добавьте данные LiveData оценки в файл score_fragment.xml

На этом этапе вы привязываете score LiveData к текстовому представлению оценки во фрагменте оценки.

  1. В score_fragment.xml добавьте атрибут android:text к текстовому представлению счёта. Присвойте атрибуту text значение scoreViewModel.score . Поскольку score — это целое число, преобразуйте его в строку с помощью String.valueOf() .
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. В ScoreFragment после инициализации scoreViewModel установите текущую активность в качестве владельца жизненного цикла переменной 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. В ScoreFragment удалите наблюдателя для объекта score .

Код для удаления:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Запустите приложение и играйте. Обратите внимание, что счёт во фрагменте счёта отображается корректно, без наблюдателя во фрагменте счёта.

Шаг 3: Добавьте форматирование строки с привязкой данных

В макете можно добавить форматирование строк и привязку данных. В этой задаче вы форматируете текущее слово, заключая его в кавычки. Также вы форматируете строку с результатом, добавляя к ней префикс «Current Score» , как показано на следующем рисунке.

  1. В string.xml добавьте следующие строки, которые будут использоваться для форматирования текстовых представлений word и score . %s и %d — это заполнители для текущего слова и текущего результата.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. В game_fragment.xml обновите атрибут text текстового представления word_text , чтобы использовать строковый ресурс quote_format . Передайте gameViewModel.word . Текущее слово будет передано в качестве аргумента в строку форматирования.
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. Отформатируйте текстовое представление score аналогично word_text . В файле game_fragment.xml добавьте атрибут text к текстовому представлению score_text . Используйте строковый ресурс score_format , который принимает один числовой аргумент, представленный заполнителем %d . Передайте объект LiveData score в качестве аргумента этой строке форматирования.
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. В классе GameFragment , внутри метода onCreateView() , удалите код наблюдателя score .

Код для удаления:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Очистите, пересоберите и запустите приложение, а затем играйте. Обратите внимание, что текущее слово и счёт отображаются на экране игры.

Поздравляем! Вы интегрировали LiveData и ViewModel с привязкой данных в своё приложение. Это позволяет представлениям в макете напрямую взаимодействовать с ViewModel , без использования обработчиков щелчков во фрагменте. Вы также использовали объекты LiveData в качестве источника привязки данных для автоматического уведомления пользовательского интерфейса об изменениях данных без использования методов наблюдателя LiveData .

Проект Android Studio: GuessTheWord

  • Библиотека привязки данных без проблем работает с компонентами архитектуры Android, такими как ViewModel и LiveData .
  • Макеты в вашем приложении могут быть привязаны к данным в компонентах архитектуры, которые уже помогают вам управлять жизненным циклом контроллера пользовательского интерфейса и уведомлять об изменениях в данных.

Привязка данных ViewModel

  • Вы можете связать ViewModel с макетом, используя привязку данных.
  • Объекты ViewModel содержат данные пользовательского интерфейса. Передавая объекты ViewModel в привязку данных, можно автоматизировать часть взаимодействия между представлениями и объектами ViewModel .

Как связать ViewModel с макетом:

  • В файле макета добавьте переменную привязки данных типа ViewModel .
   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • В файле GameFragment передайте GameViewModel в привязку данных.
binding.gameViewModel = viewModel

Привязки прослушивателя

  • Привязки прослушивателя — это выражения привязки в макете, которые запускаются при возникновении событий щелчка, таких как onClick() .
  • Привязки прослушивателя записываются в виде лямбда-выражений.
  • Используя привязки прослушивателей, вы заменяете прослушиватели щелчков в контроллерах пользовательского интерфейса привязками прослушивателей в файле макета.
  • Привязка данных создает прослушиватель и устанавливает его в представлении.
 android:onClick="@{() -> gameViewModel.onSkip()}"

Добавление LiveData к привязке данных

  • Объекты LiveData можно использовать в качестве источника привязки данных для автоматического уведомления пользовательского интерфейса об изменениях в данных.
  • Вы можете привязать представление непосредственно к объекту LiveData в ViewModel . При изменении LiveData в ViewModel представления в макете могут обновляться автоматически, без использования методов наблюдателя в контроллерах пользовательского интерфейса.
android:text="@{gameViewModel.word}"
  • Чтобы заработала привязка данных LiveData , установите текущую активность (контроллер пользовательского интерфейса) в качестве владельца жизненного цикла переменной binding в контроллере пользовательского интерфейса.
binding.lifecycleOwner = this

Форматирование строк с привязкой данных

  • Используя привязку данных, вы можете отформатировать строковый ресурс с помощью заполнителей, например %s для строк и %d для целых чисел.
  • Чтобы обновить text атрибут представления, передайте объект LiveData в качестве аргумента строки форматирования.
 android:text="@{@string/quote_format(gameViewModel.word)}"

Курс Udacity:

Документация для разработчиков Android:

В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.

Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.

Ответьте на эти вопросы

Вопрос 1

Какое из следующих утверждений неверно в отношении привязок прослушивателя?

  • Привязки прослушивателя — это выражения привязки, которые выполняются при возникновении события.
  • Привязки прослушивателя работают со всеми версиями плагина Android Gradle.
  • Привязки прослушивателя записываются в виде лямбда-выражений.
  • Привязки прослушивателей похожи на ссылки на методы, но они позволяют запускать произвольные выражения привязки данных.

Вопрос 2

Предположим, что ваше приложение включает этот строковый ресурс:
<string name="generic_name">Hello %s</string>

Какой из приведенных ниже вариантов синтаксиса является правильным для форматирования строки с использованием выражения привязки данных?

  • 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}"

Вопрос 3

Когда вычисляется и выполняется выражение привязки прослушивателя?

  • При изменении данных, хранящихся в LiveData
  • Когда действие создается заново из-за изменения конфигурации
  • Когда происходит событие, такое как onClick()
  • Когда активность уходит на второй план

Начните следующий урок: 5.4: Преобразования LiveData

Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по основам Android Kotlin .