Основы Android Kotlin 05.1: ViewModel и ViewModelFactory

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

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

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

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

Введение

В этой лабораторной работе вы познакомитесь с одним из компонентов архитектуры Android — ViewModel :

  • Класс ViewModel используется для хранения и управления данными пользовательского интерфейса с учётом жизненного цикла. Класс ViewModel позволяет данным сохраняться при изменениях конфигурации устройства, таких как поворот экрана и изменение доступности клавиатуры.
  • Класс ViewModelFactory используется для создания и возврата объекта ViewModel , который сохраняется при изменении конфигурации.

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

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

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

  • Как использовать рекомендуемую архитектуру приложений Android.
  • Как использовать классы Lifecycle , ViewModel и ViewModelFactory в вашем приложении.
  • Как сохранить данные пользовательского интерфейса при изменении конфигурации устройства.
  • Что такое шаблон проектирования «Фабричный метод» и как его использовать.
  • Как создать объект ViewModel с помощью интерфейса ViewModelProvider.Factory .

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

  • Добавьте ViewModel в приложение, чтобы сохранить данные приложения и сохранить их при изменении конфигурации.
  • Используйте ViewModelFactory и шаблон проектирования «фабричный метод» для создания экземпляра объекта ViewModel с параметрами конструктора.

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

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

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

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

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

В этом задании вы загрузите и запустите стартовое приложение и изучите код.

Шаг 1: Начало работы

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

Шаг 2: Проведите пошаговый разбор кода

  1. В Android Studio изучите код, чтобы понять, как работает приложение.
  2. Обязательно ознакомьтесь с файлами, описанными ниже, они особенно важны.

MainActivity.kt

Этот файл содержит только код по умолчанию, сгенерированный шаблоном.

res/layout/main_activity.xml

Этот файл содержит основной макет приложения. NavHostFragment размещает остальные фрагменты по мере перемещения пользователя по приложению.

Фрагменты пользовательского интерфейса

Стартовый код состоит из трех фрагментов в трех различных пакетах в пакете com.example.android.guesstheword.screens :

  • title/TitleFragment для титульного экрана
  • game/GameFragment для игрового экрана
  • score/ScoreFragment для экрана счета

экраны/title/TitleFragment.kt

Заголовочный фрагмент — это первый экран, отображаемый при запуске приложения. Обработчик нажатия установлен на кнопку «Играть» для перехода к экрану игры.

screens/game/GameFragment.kt

Это основной фрагмент, где происходит большая часть действия игры:

  • Переменные определяются для текущего слова и текущего счета.
  • Список wordList , определенный внутри метода resetList() представляет собой пример списка слов, которые будут использоваться в игре.
  • Метод onSkip() — это обработчик нажатия кнопки «Пропустить» . Он уменьшает счёт на 1, а затем отображает следующее слово с помощью метода nextWord() .
  • Метод onCorrect() — это обработчик нажатия кнопки « Понял» . Этот метод реализован аналогично методу onSkip() . Единственное отличие заключается в том, что этот метод добавляет 1 к счёту, а не вычитает.

screens/score/ScoreFragment.kt

ScoreFragment — это финальный экран в игре, отображающий итоговый счёт игрока. В этой лабораторной работе вы добавите реализацию для отображения этого экрана и итогового счёта.

res/navigation/main_navigation.xml

Граф навигации показывает, как фрагменты связаны посредством навигации:

  • Из фрагмента заголовка пользователь может перейти к фрагменту игры.
  • Из игрового фрагмента пользователь может перейти к фрагменту с результатами.
  • Из фрагмента счета пользователь может вернуться к фрагменту игры.

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

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

Проблемы в приложении:

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

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

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

Архитектура приложения — это способ проектирования классов приложения и взаимосвязей между ними, обеспечивающий организованность кода, его эффективность в определённых сценариях и удобство работы с ним. В этом наборе из четырёх практических работ вы внесёте улучшения в приложение GuessTheWord, следуя рекомендациям по архитектуре приложений для Android , используя компоненты архитектуры Android . Архитектура приложения для Android аналогична архитектурному шаблону MVVM (модель-представление-модель представления).

Приложение GuessTheWord следует принципу разделения ответственности и разделено на классы, каждый из которых отвечает за отдельную задачу. В этой первой практической работе урока вы будете работать с классами UI-контроллера, ViewModel и ViewModelFactory .

Контроллер пользовательского интерфейса

Контроллер пользовательского интерфейса (UI) — это класс, основанный на пользовательском интерфейсе, такой как Activity или Fragment . UI-контроллер должен содержать только логику, обрабатывающую взаимодействие с пользовательским интерфейсом и операционной системой, например, отображение представлений и захват пользовательского ввода. Не помещайте в UI-контроллер логику принятия решений, например, логику, определяющую отображаемый текст.

В стартовом коде GuessTheWord контроллерами пользовательского интерфейса являются три фрагмента: GameFragment , ScoreFragment, и TitleFragment . Следуя принципу «разделения ответственности», GameFragment отвечает только за отрисовку игровых элементов на экране и распознавание нажатий кнопок пользователем, и ничего более. Когда пользователь нажимает кнопку, эта информация передаётся в GameViewModel .

ViewModel

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

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

ViewModelFactory

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

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

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

В этом задании вы добавите в приложение свою первую ViewModelGameViewModel для GameFragment . Вы также узнаете, что означает, что ViewModel учитывает жизненный цикл.

Шаг 1: Добавьте класс GameViewModel

  1. Откройте файл build.gradle(module:app) . В блоке dependencies добавьте зависимость Gradle для ViewModel .

    Если вы используете последнюю версию библиотеки, приложение-решение должно скомпилироваться как положено. Если это не так, попробуйте решить проблему самостоятельно или вернитесь к версии, указанной ниже.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. В папке screens/game/ создайте новый класс Kotlin с именем GameViewModel .
  2. Сделайте так, чтобы класс GameViewModel расширял абстрактный класс ViewModel .
  3. Чтобы лучше понять, как ViewModel учитывает жизненный цикл, добавьте блок init с оператором log .
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Шаг 2: Переопределите onCleared() и добавьте ведение журнала

ViewModel уничтожается при отсоединении связанного фрагмента или при завершении активности. Непосредственно перед уничтожением ViewModel вызывается метод обратного вызова onCleared() для очистки ресурсов.

  1. В классе GameViewModel переопределите метод onCleared() .
  2. Добавьте оператор журнала в onCleared() для отслеживания жизненного цикла GameViewModel .
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Шаг 3: Свяжите GameViewModel с фрагментом игры

ViewModel необходимо связать с контроллером пользовательского интерфейса. Для этого необходимо создать ссылку на ViewModel внутри контроллера пользовательского интерфейса.

На этом этапе вы создаете ссылку на GameViewModel внутри соответствующего контроллера пользовательского интерфейса, которым является GameFragment .

  1. В классе GameFragment добавьте поле типа GameViewModel на верхнем уровне в качестве переменной класса.
private lateinit var viewModel: GameViewModel

Шаг 4: Инициализация ViewModel

При изменении конфигурации, например, при повороте экрана, контроллеры пользовательского интерфейса, такие как фрагменты, создаются заново. Однако экземпляры ViewModel сохраняются. Если вы создаёте экземпляр ViewModel с помощью класса ViewModel , каждый раз при повторном создании фрагмента создаётся новый объект. Вместо этого создайте экземпляр ViewModel с помощью ViewModelProvider .

Как работает ViewModelProvider :

  • ViewModelProvider возвращает существующую ViewModel , если она существует, или создает новую, если она еще не существует.
  • ViewModelProvider создает экземпляр ViewModel в связи с заданной областью действия (действием или фрагментом).
  • Созданная ViewModel сохраняется до тех пор, пока существует область действия. Например, если область действия представляет собой фрагмент, ViewModel сохраняется до тех пор, пока фрагмент не будет отсоединен.

Инициализируйте ViewModel , используя метод ViewModelProviders.of() для создания ViewModelProvider :

  1. В классе GameFragment инициализируйте переменную viewModel . Поместите этот код в onCreateView() после определения переменной привязки. Используйте метод ViewModelProviders.of() и передайте ему связанный контекст GameFragment и класс GameViewModel .
  2. Над инициализацией объекта ViewModel добавьте оператор журнала для регистрации вызова метода ViewModelProviders.of() .
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Запустите приложение. В Android Studio откройте панель Logcat и выберите фильтр по параметру Game . Нажмите кнопку «Воспроизвести» на устройстве или эмуляторе. Откроется экран игры.

    Как показано в Logcat, метод onCreateView() объекта GameFragment вызывает метод ViewModelProviders.of() для создания GameViewModel . Операторы журналирования, добавленные в GameFragment и GameViewModel отображаются в Logcat.

  1. Включите функцию автоматического поворота на устройстве или эмуляторе и несколько раз измените ориентацию экрана. GameFragment каждый раз уничтожается и создаётся заново, поэтому ViewModelProviders.of() вызывается каждый раз. Однако GameViewModel создаётся только один раз и не пересоздаётся и не уничтожается при каждом вызове.
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. Выйти из игры или выйти из игрового фрагмента. GameFragment уничтожается. Связанная с ним GameViewModel также уничтожается, и вызывается метод обратного вызова 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!

ViewModel сохраняет работоспособность при изменении конфигурации, поэтому это хорошее место для данных, которым необходимо сохранять работоспособность при изменении конфигурации:

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

Для сравнения, вот как обрабатываются данные пользовательского интерфейса GameFragment в стартовом приложении до добавления ViewModel и после добавления ViewModel :

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

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

Шаг 1: Перемещение полей данных и обработки данных во ViewModel

Переместите следующие поля данных и методы из GameFragment в GameViewModel :

  1. Переместите поля данных « word , score и wordList . Убедитесь, что поля word и score не являются private .

    Не перемещайте переменную привязки GameFragmentBinding , поскольку она содержит ссылки на представления. Эта переменная используется для расширения макета, настройки обработчиков щелчков и отображения данных на экране — это входит в обязанности фрагмента.
  2. Переместите методы resetList() и nextWord() . Эти методы определяют, какое слово отображать на экране.
  3. Изнутри метода onCreateView() переместите вызовы методов resetList() и nextWord() в блок init объекта GameViewModel .

    Эти методы должны находиться в блоке init , поскольку список слов следует сбрасывать при создании ViewModel , а не при каждом создании фрагмента. Вы можете удалить оператор log в блоке init объекта GameFragment .

Обработчики нажатий onSkip() и onCorrect() в GameFragment содержат код для обработки данных и обновления пользовательского интерфейса. Код обновления пользовательского интерфейса должен остаться во фрагменте, но код обработки данных необходимо перенести во ViewModel .

На данный момент поместите в обоих местах одинаковые методы:

  1. Скопируйте методы onSkip() и onCorrect() из GameFragment в GameViewModel .
  2. В GameViewModel убедитесь, что методы onSkip() и onCorrect() не являются private , так как вы будете ссылаться на эти методы из фрагмента.

Вот код класса GameViewModel после рефакторинга:

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!")
   }
}

Вот код класса GameFragment после рефакторинга:

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

Шаг 2: Обновите ссылки на обработчики щелчков и поля данных в GameFragment.

  1. В GameFragment обновите методы onSkip() и onCorrect() . Удалите код обновления счёта и вместо этого вызовите соответствующие методы onSkip() и onCorrect() в viewModel .
  2. Поскольку вы переместили метод nextWord() в ViewModel , фрагмент игры больше не может получить к нему доступ.

    В методах onSkip() и onCorrect() GameFragment замените вызов nextWord() на updateScoreText() и updateWordText() . Эти методы выводят данные на экран.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. В GameFragment обновите переменные score и word , чтобы использовать переменные GameViewModel , поскольку эти переменные теперь находятся в GameViewModel .
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. В GameViewModel , внутри метода nextWord() , удалите вызовы методов updateWordText() и updateScoreText() . Теперь эти методы вызываются из GameFragment .
  2. Соберите приложение и убедитесь в отсутствии ошибок. Если ошибки есть, очистите и пересоберите проект.
  3. Запустите приложение и сыграйте в игру, используя несколько слов. Находясь на экране игры, поверните устройство. Обратите внимание, что текущий счёт и слово сохраняются после смены ориентации.

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

В этой задаче вы реализуете прослушиватель щелчков для кнопки «Завершить игру» .

  1. В GameFragment добавьте метод onEndGame() . Метод onEndGame() будет вызываться, когда пользователь нажмёт кнопку «Завершить игру» .
private fun onEndGame() {
   }
  1. В методе onCreateView() класса GameFragment найдите код, который устанавливает обработчики щелчков для кнопок « Понял» и «Пропустить» . Прямо под этими двумя строками установите обработчик щелчков для кнопки «Завершить игру» . Используйте переменную привязки binding . Внутри обработчика щелчков вызовите метод onEndGame() .
binding.endGameButton.setOnClickListener { onEndGame() }
  1. В GameFragment добавьте метод gameFinished() для перехода приложения к экрану счёта. Передайте счёт в качестве аргумента с помощью 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. В методе onEndGame() вызовите метод gameFinished() .
private fun onEndGame() {
   gameFinished()
}
  1. Запустите приложение, играйте в игру и циклично перебирайте слова. Нажмите кнопку «Завершить игру» . Обратите внимание, что приложение переходит на экран счёта, но итоговый счёт не отображается. Исправьте это в следующем задании.

Когда пользователь завершает игру, ScoreFragment не отображает счёт. Вам нужно, чтобы ViewModel хранила счёт, отображаемый ScoreFragment . Значение счёта будет передано во время инициализации ViewModel с помощью шаблона «фабричный метод» .

Шаблон «Фабричный метод» — это порождающий шаблон проектирования , использующий фабричные методы для создания объектов. Фабричный метод — это метод, возвращающий экземпляр того же класса.

В этой задаче вы создадите ViewModel с параметризованным конструктором для фрагмента счета и фабричным методом для создания экземпляра ViewModel .

  1. В пакете score создайте новый класс Kotlin с именем ScoreViewModel . Этот класс будет ViewModel для фрагмента score.
  2. Расширьте класс ScoreViewModel из ViewModel. Добавьте параметр конструктора для итоговой оценки. Добавьте блок init с оператором log.
  3. В классе ScoreViewModel добавьте переменную с именем score для сохранения итогового результата.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. В пакете score создайте ещё один класс Kotlin с именем ScoreViewModelFactory . Этот класс будет отвечать за создание экземпляра объекта ScoreViewModel .
  2. Расширьте класс ScoreViewModelFactory от ViewModelProvider.Factory и добавьте параметр конструктора для итоговой оценки.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. В ScoreViewModelFactory Android Studio выдаёт ошибку о нереализованном абстрактном члене. Чтобы устранить эту ошибку, переопределите метод create() . В методе create() верните только что созданный объект 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. В ScoreFragment создайте переменные класса для ScoreViewModel и ScoreViewModelFactory .
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. В ScoreFragment , внутри onCreateView() , после инициализации переменной binding , инициализируйте viewModelFactory . Используйте ScoreViewModelFactory . Передайте окончательный результат из набора аргументов в качестве параметра конструктора в ScoreViewModelFactory() .
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. В onCreateView( ) после инициализации viewModelFactory инициализируйте объект viewModel . Вызовите метод ViewModelProviders.of() , передайте ему контекст фрагмента счёта и viewModelFactory . Это создаст объект ScoreViewModel с помощью фабричного метода, определённого в классе viewModelFactory .
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. В методе onCreateView() после инициализации viewModel установите текст представления scoreText равным итоговой оценке, определенной в ScoreViewModel .
binding.scoreText.text = viewModel.score.toString()
  1. Запустите приложение и играйте. Перебирайте несколько или все слова и нажмите «Завершить игру» . Обратите внимание, что фрагмент счёта теперь отображает итоговый счёт.

  1. Необязательно: проверьте логи ScoreViewModel в Logcat, отфильтровав их по ScoreViewModel . Значение оценки должно отображаться.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

В этом задании вы реализовали ScoreFragment для использования ViewModel . Вы также узнали, как создать параметризованный конструктор для ViewModel с помощью интерфейса ViewModelFactory .

Поздравляем! Вы изменили архитектуру своего приложения, чтобы использовать один из компонентов архитектуры Android — ViewModel . Вы решили проблему жизненного цикла приложения, и теперь данные игры сохраняются при изменении конфигурации. Вы также узнали, как создать параметризованный конструктор для создания ViewModel с помощью интерфейса ViewModelFactory .

Проект Android Studio: GuessTheWord

  • В рекомендациях по архитектуре приложений для Android рекомендуется разделять классы, имеющие разные обязанности.
  • Контроллер пользовательского интерфейса (UI Controller) — это класс, основанный на пользовательском интерфейсе, такой как Activity или Fragment . Контроллеры UI должны содержать только логику, обрабатывающую взаимодействие с пользовательским интерфейсом и операционной системой; они не должны содержать данные для отображения в пользовательском интерфейсе. Поместите эти данные во ViewModel .
  • Класс ViewModel хранит и управляет данными, связанными с пользовательским интерфейсом. Класс ViewModel позволяет данным сохраняться при изменении конфигурации, например при повороте экрана.
  • ViewModel — один из рекомендуемых компонентов архитектуры Android .
  • ViewModelProvider.Factory — это интерфейс, который можно использовать для создания объекта ViewModel .

В таблице ниже сравниваются контроллеры пользовательского интерфейса с экземплярами ViewModel , которые хранят для них данные:

Контроллер пользовательского интерфейса

ViewModel

Примером контроллера пользовательского интерфейса является ScoreFragment , созданный вами в этой лабораторной работе.

Примером ViewModel является ScoreViewModel , созданный вами в этой лабораторной работе.

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

Содержит данные, которые контроллер пользовательского интерфейса отображает в пользовательском интерфейсе.

Содержит код для отображения данных и код пользовательских событий, такой как прослушиватели щелчков.

Содержит код для обработки данных.

Уничтожается и создается заново при каждом изменении конфигурации.

Уничтожается только тогда, когда связанный с ним контроллер пользовательского интерфейса окончательно исчезает — для активности, когда активность завершается, или для фрагмента, когда фрагмент отсоединяется.

Содержит представления.

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

Содержит ссылку на соответствующую ViewModel .

Не содержит ссылок на соответствующий контроллер пользовательского интерфейса.

Курс Udacity:

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

Другой:

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

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

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

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

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

Вопрос 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Вопрос 2

ViewModel никогда не должен содержать ссылок на фрагменты, действия или представления. Верно или неверно?

  • Истинный
  • ЛОЖЬ

Вопрос 3

Когда ViewModel уничтожается?

  • Когда связанный контроллер пользовательского интерфейса уничтожается и создается заново во время изменения ориентации устройства.
  • В смене ориентации.
  • Когда связанный контроллер пользовательского интерфейса завершен (если это активность) или отсоединен (если это фрагмент).
  • Когда пользователь нажимает кнопку «Назад».

Вопрос 4

Для чего нужен интерфейс ViewModelFactory ?

  • Создание объекта ViewModel .
  • Сохранение данных при изменении ориентации.
  • Обновление данных, отображаемых на экране.
  • Получение уведомлений при изменении данных приложения.

Начните следующий урок: 5.2: LiveData и наблюдатели LiveData

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