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

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

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

Экран игры

Экран счета

Введение

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

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

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

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

Что вы узнаете

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

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

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

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

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

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

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

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

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

Шаг 1: Начните

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

Шаг 2. Пошаговое руководство по коду

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

MainActivity.kt

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

рез/макет/main_activity.xml

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

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

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

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

экраны/название/TitleFragment.kt

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

экраны/игра/GameFragment.kt

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

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

экраны/оценка/ScoreFragment.kt

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

рез/навигация/main_navigation.xml

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

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

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

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

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

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

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

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

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

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

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

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

В начальном коде 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 , а не каждый раз при создании фрагмента. Вы можете удалить оператор журнала в блоке 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 , фрагмент игры больше не может получить к нему доступ.

    В GameFragment в onSkip() и onCorrect() замените вызов 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. В GameFragment внутри onCreateView() найдите код, устанавливающий прослушиватели кликов для кнопок Got It и Skip . Прямо под этими двумя строками установите прослушиватель кликов для кнопки « Завершить игру» . Используйте переменную привязки, 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 для фрагмента партитуры.
  2. Расширьте класс ScoreViewModel от ViewModel. Добавьте параметр конструктора для окончательной оценки. Добавьте блок init с оператором журнала.
  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 рекомендуется разделять классы с разными обязанностями.
  • Контроллер пользовательского интерфейса — это класс на основе пользовательского интерфейса, такой как Activity или Fragment . Контроллеры пользовательского интерфейса должны содержать только логику, которая обрабатывает взаимодействие пользовательского интерфейса и операционной системы; они не должны содержать данные для отображения в пользовательском интерфейсе. Поместите эти данные в ViewModel .
  • Класс ViewModel хранит данные, связанные с пользовательским интерфейсом, и управляет ими. Класс ViewModel позволяет данным сохраняться после изменений конфигурации, таких как повороты экрана.
  • ViewModel является одним из рекомендуемых компонентов архитектуры Android .
  • ViewModelProvider.Factory — это интерфейс, который можно использовать для создания объекта ViewModel .

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

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

ViewModel

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

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

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

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

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

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

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

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

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

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

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

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

Удасити курс:

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

Другой:

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

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

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

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

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

Вопрос 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

вопрос 2

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

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

Вопрос 3

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

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

Вопрос 4

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

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

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

Ссылки на другие лаборатории кода в этом курсе см. на целевой странице лаборатории кода Android Kotlin Fundamentals .