Android Kotlin 基础知识 05.1:ViewModel 和 ViewModelFactory

此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。

广告标题画面

游戏屏幕

分数屏幕

简介

在此 Codelab 中,您将了解其中一个 Android 架构组件 ViewModel

  • 您可以使用 ViewModel 类以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生设备配置更改(如屏幕旋转和键盘可用性更改)后继续存在。
  • 您可以使用 ViewModelFactory 类来实例化并返回 ViewModel,该对象在配置更改后保持不变。

您应当已掌握的内容

  • 如何使用 Kotlin 创建基本的 Android 应用?
  • 如何使用导航图在应用中实现导航。
  • 如何添加代码以在应用的目的地之间导航以及在导航目的地之间传递数据。
  • activity 和 fragment 生命周期如何工作。
  • 如何在 Android Studio 中使用 Logcat 向应用添加日志记录信息和读取日志?

学习内容

  • 如何使用推荐的 Android 应用架构
  • 如何在应用中使用 LifecycleViewModelViewModelFactory 类?
  • 如何通过设备配置更改保留界面数据。
  • 什么是工厂方法设计模式以及如何使用它。
  • 如何使用接口 ViewModelProvider.Factory 创建 ViewModel 对象。

您将执行的操作

  • ViewModel 添加到应用中,以保存应用数据,以使数据在配置更改后仍然存在。
  • 使用 ViewModelFactory 和工厂方法设计模式,以构造函数参数实例化 ViewModel 对象。

在第 5 课 Codelab 中,您将开发 GuessTheWord 应用(从起始代码开始)。GuessTheWord 是一款双人角色扮演式游戏,玩家必须相互协作才能尽可能赢得最高分。

第一个玩家会检查应用中的单词,然后轮流执行每个单词,确保不会向第二个玩家显示单词。第二个玩家猜词。

玩游戏时,第一个玩家在设备上打开应用并看到“吉他”字样,如以下屏幕截图所示。

第一个玩家会说出单词,注意不要实际说出单词。

  • 当第二个玩家猜出这个单词后,第一个玩家会按 Got It(知道了)按钮,这会将计数增加 1 并显示下一个单词。
  • 如果第二个玩家猜不出单词,第一个玩家会按 Skip 按钮,这会将计数减少 1,并跳至下一个单词。
  • 如需结束游戏,请按结束游戏按钮。(本系列第一个 Codelab 的起始代码中不包含此功能。)

在此任务中,您将下载并运行起始应用并检查代码。

第 1 步:开始使用

  1. 下载 GuessTheWord 起始代码并在 Android Studio 中打开项目。
  2. 在 Android 设备或模拟器上运行应用。
  3. 点按按钮。请注意,Skip 按钮会显示下一个单词并降低分数,而 Got It 按钮会显示下一个单词并将得分增加 1。结束游戏按钮未实现,因此当您点按它时不会执行任何操作。

第 2 步:执行代码演示

  1. 在 Android Studio 中,探索代码以了解应用的工作原理。
  2. 请务必查看下述文件,这些文件特别重要。

MainActivity.kt

此文件仅包含模板生成的默认代码。

res/layout/main_activity.xml

此文件包含应用的主要布局。当用户在应用中导航时,NavHostFragment 会托管其他 Fragment。

界面 Fragment

起始代码在 com.example.android.guesstheword.screens 软件包下的三个不同软件包中具有三个 fragment:

  • title/TitleFragment(适用于标题屏幕)
  • game/GameFragment,适用于游戏屏幕
  • score/ScoreFragment,用于得分屏幕

screen/title/TitleFragment.kt

标题 fragment 是应用启动时显示的第一个屏幕。将点击处理程序设置为 Play 按钮,以导航到游戏屏幕。

screen/game/GameFragment.kt

这是主要 fragment,其中大多数游戏操作发生:

  • 为当前单词和当前得分定义变量。
  • resetList() 方法中定义的 wordList 是要在游戏中使用的单词的示例列表。
  • onSkip() 方法是 Skip 按钮的点击处理程序。它会将得分减少 1,然后使用 nextWord() 方法显示下一个字词。
  • onCorrect() 方法是 Got It 按钮的点击处理程序。此方法的实现方式与 onSkip() 方法类似。唯一的区别在于,此方法会在得分的基础上加 1,而不是减去。

screen/score/ScoreFragment.kt

ScoreFragment 是游戏中的最后一个屏幕,显示玩家的最终得分。在此 Codelab 中,您将添加显示此屏幕和显示最终得分的实现。

res/navigation/main_navigation.xml

导航图显示了 fragment 如何通过导航连接:

  • 从标题 fragment 可以导航到游戏 fragment。
  • 从游戏 fragment,用户可以导航到得分 fragment。
  • 用户可通过得分 Fragment 返回到游戏 Fragment。

在此任务中,您将查找 GuessTheWord 起始应用的问题。

  1. 运行起始代码并玩完几个单词,在猜完每个单词后点按跳过知道了
  2. 游戏画面现在会显示一个单词和当前得分。此时通过旋转设备或模拟器更改屏幕方向,请注意,当前得分已丢失。
  3. 再猜几个单词。当游戏屏幕显示某个得分时,关闭并重新打开应用。请注意,游戏不会从头开始保存,因为系统不会保存应用状态。
  4. 玩一下游戏,猜几个单词,然后点按结束游戏按钮。请注意,没有任何反应。

应用中存在的问题:

  • 起始应用不会在配置更改期间(例如设备屏幕方向发生变化或应用关闭后重启时)保存和恢复应用状态。
    您可以使用 onSaveInstanceState() 回调来解决此问题。不过,使用 onSaveInstanceState() 方法需要编写额外的代码以在 bundle 中保存状态,并实现用于检索该状态的逻辑。此外,可以存储的数据量极少。
  • 用户点按结束游戏按钮时,游戏屏幕未转到分数屏幕。

您可以使用此 Codelab 中了解的应用架构组件来解决这些问题。

应用架构

应用架构是一种设计应用类及其关系的方式,以便代码井然有序、在特定情况下表现出色且易于使用。在这组 Codelab(共四个)中,您对 GuessTheWord 应用所做的改进遵循了 Android 应用架构准则,并且您使用的是 Android 架构组件。Android 应用架构类似于 MVVM (model-view-viewmodel) 架构模式。

GuessTheWord 应用遵循分离关注点设计原则,分为多个类,每个类解决一个独立的问题。在本课的第一个 Codelab 中,您使用的类是界面控制器、ViewModelViewModelFactory

界面控制器

界面控制器是基于界面的类,例如 ActivityFragment界面控制器应仅包含处理界面和操作系统交互(例如显示视图和捕获用户输入)的逻辑。不要将决策逻辑(例如确定要显示的文本的逻辑)放入界面控制器中。

在 GuessTheWord 起始代码中,界面控制器是三个 fragment:GameFragmentScoreFragment,TitleFragment。根据“分离关注点”设计原则,GameFragment 仅负责将游戏元素绘制到屏幕上,以及了解用户何时点按按钮,而不执行任何操作。当用户点按按钮时,此信息会传递给 GameViewModel

ViewModel

ViewModel 用于存储要在与 ViewModel 关联的 fragment 或 activity 中显示的数据。ViewModel 可对数据执行简单的计算和转换,以准备好数据供界面控制器进行显示。在此架构中,ViewModel 执行决策。

GameViewModel 包含得分值、字词列表和当前字词等数据,因为这是要在屏幕上显示的数据。GameViewModel 还包含用于执行简单计算以确定数据当前状态的业务逻辑。

ViewModelFactory

ViewModelFactory 可实例化 ViewModel 对象(带或不带构造函数参数)。

在后续 Codelab 中,您将了解与界面控制器和 ViewModel 相关的其他 Android 架构组件。

ViewModel 类旨在存储和管理与界面相关的数据。在此应用中,每个 ViewModel 都与一个 fragment 相关联。

在此任务中,您将向应用添加第一个 ViewModel,即 GameFragmentGameViewModel。您还将了解 ViewModel 具有生命周期感知能力意味着什么。

第 1 步:添加 GameViewModel 类

  1. 打开 build.gradle(module:app) 文件。在 dependencies 块内,添加 ViewModel 的 Gradle 依赖项。

    如果您使用该库的最新版本,解决方案应用应按预期编译。如果不能解决问题,请尝试 解决 问题,或恢复如下所示的版本。
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. 在软件包 screens/game/ 文件夹中,新建一个名为 GameViewModel 的 Kotlin 类。
  2. 使 GameViewModel 类扩展抽象类 ViewModel
  3. 为了帮助您更好地了解 ViewModel 如何具有生命周期感知能力,请添加包含 log 语句的 init 代码块。
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

第 2 步:替换 onCleared() 并添加日志记录

当关联的 fragment 分离或 activity 结束时,ViewModel 会被销毁。在 ViewModel 被销毁之前,系统会调用 onCleared() 回调来清理资源。

  1. GameViewModel 类中,替换 onCleared() 方法。
  2. onCleared() 内添加日志语句,以跟踪 GameViewModel 生命周期。
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

第 3 步:将 GameViewModel 与游戏 Fragment 相关联

ViewModel 需要与界面控制器相关联。为了关联这两者,您可以在界面控制器内创建对 ViewModel 的引用。

在此步骤中,您将在相应的界面控制器(即 GameFragment)内创建对 GameViewModel 的引用。

  1. GameFragment 类中,在顶层添加一个 GameViewModel 类型的字段作为类变量。
private lateinit var viewModel: GameViewModel

第 4 步:初始化 ViewModel

在屏幕旋转等配置更改期间,界面控制器(例如 Fragment)会重新创建。但是,ViewModel 个实例仍然存在。如果您使用 ViewModel 类创建 ViewModel 实例,则每次重新创建 Fragment 时都会创建一个新对象。而应使用 ViewModelProvider 创建 ViewModel 实例。

ViewModelProvider 的运作方式:

  • ViewModelProvider 会返回一个现有的 ViewModel(如果存在),或创建一个新的 ViewModel(如果不存在)。
  • ViewModelProvider 会创建一个与指定范围(activity 或 fragment)相关联的 ViewModel 实例。
  • 只要范围处于活动状态,就会保留创建的 ViewModel。例如,如果作用域是 fragment,ViewModel 会一直保留,直到该 fragment 分离。

初始化 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 进行过滤。点按设备或模拟器上的 Play 按钮。游戏屏幕打开。

    如 Logcat 中所示,GameFragmentonCreateView() 方法会调用 ViewModelProviders.of() 方法以创建 GameViewModel。您在 GameFragmentGameViewModel 中添加的日志记录语句会显示在 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. 退出游戏或退出游戏 fragment。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 绝不应包含对 Fragment、Activity 或视图的引用,因为 Activity、Fragment 和视图在配置更改后不会保留。

为便于比较,我们先介绍了在添加 ViewModel 之前以及添加 ViewModel 后如何处理起始应用中的 GameFragment 界面数据:

  • 添加 ViewModel 之前:
    当应用经过屏幕旋转等配置更改时,游戏 fragment 会被销毁并重新创建。数据会丢失。
  • 添加 ViewModel 并将游戏 fragment 的界面数据移动到 ViewModel 之后:
    fragment 需要显示的所有数据现在都是 ViewModel。当应用发生配置更改后,ViewModel 仍然存在,但数据会保留。

在此任务中,您需要将应用的界面数据连同处理数据的方法一起移入 GameViewModel 类。这样做可以在配置更改期间保留数据。

第 1 步:将数据字段和数据处理移至 ViewModel

将以下数据字段和方法从 GameFragment 移至 GameViewModel

  1. 移动 wordscorewordList 数据字段。请确保 wordscore 的值不是 private

    请勿移动绑定变量 GameFragmentBinding,因为它包含对视图的引用。此变量用于膨胀布局、设置点击监听器以及在屏幕上显示数据(即 Fragment 的责任)。
  2. 移动 resetList()nextWord() 方法。这些方法决定要在屏幕上显示哪些字词。
  3. onCreateView() 方法内,将对 resetList()nextWord() 的方法调用移至 GameViewModelinit 代码块。

    这些方法必须位于 init 代码块中,因为您应在创建 ViewModel 时重置单词列表,而不是每次创建 fragment 时都重置单词列表。您可以删除 GameFragmentinit 块中的日志语句。

GameFragment 中的 onSkip()onCorrect() 点击处理程序包含用于处理数据和更新界面的代码。用于更新界面的代码应保留在 fragment 中,但处理数据的代码需要移至 ViewModel 中。

现在,在这两个地方放置相同的方法:

  1. onSkip()onCorrect() 方法从 GameFragment 复制到 GameViewModel
  2. GameViewModel 中,请确保 onSkip()onCorrect() 方法不是 private,因为您将从 fragment 引用这些方法。

重构后,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() 方法。移除用于更新得分的代码,并改为在 viewModel 上调用相应的 onSkip()onCorrect() 方法。
  2. 由于您将 nextWord() 方法移至 ViewModel,因此游戏 Fragment 无法再对其进行访问。

    GameFragmentonSkip()onCorrect() 方法中,将对 nextWord() 的调用替换为 updateScoreText()updateWordText()。这些方法会在屏幕上显示数据。
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. GameFragment 中,更新 scoreword 变量以使用 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 按钮设置点击监听器的代码。在这两行下方,为 End Game 按钮设置点击监听器。使用绑定变量 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,其中包含用于得分 Fragment 的参数化构造函数,以及用于实例化 ViewModel 的工厂方法。

  1. score 软件包下,新建一个名为 ScoreViewModel 的 Kotlin 类。此类将是得分 Fragment 的 ViewModel
  2. ViewModel. 扩展 ScoreViewModel 类。为最终得分添加构造函数参数。添加一个包含日志语句的 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 软件包下,再创建一个名为 ScoreViewModelFactory 的 Kotlin 类。此类将负责实例化 ScoreViewModel 对象。
  2. ViewModelProvider.Factory 扩展 ScoreViewModelFactory 类。为最终得分添加构造函数参数。
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 中,为 ScoreViewModelScoreViewModelFactory 创建类变量。
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() 方法,传入关联的得分 Fragment 上下文和 viewModelFactory。这将使用 viewModelFactory 类中定义的工厂方法创建 ScoreViewModel 对象.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. onCreateView() 方法中,初始化 viewModel 后,将 scoreText 视图的文本设置为 ScoreViewModel 中定义的最终得分。
binding.scoreText.text = viewModel.score.toString()
  1. 运行应用并玩游戏。循环浏览部分或全部字词,然后点按结束游戏。请注意,分数 Fragment 现在显示最终分数。

  1. 可选:通过过滤 ScoreViewModel 检查 Logcat 中的 ScoreViewModel 日志。系统会显示分数值。
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

在此任务中,您实现了 ScoreFragment 以使用 ViewModel。您还学习了如何使用 ViewModelFactory 接口为 ViewModel 创建参数化构造函数。

恭喜!您已将应用的架构更改为使用其中一个 Android 架构组件 ViewModel。您解决了应用的生命周期问题,现在游戏的数据在配置更改后仍然存在。您还学习了如何使用 ViewModelFactory 接口创建参数化构造函数,以便创建 ViewModel

Android Studio 项目:GuessTheWord

  • Android 应用架构准则建议区分具有不同责任的类。
  • 界面控制器是基于界面的类,例如 ActivityFragment。界面控制器应仅包含处理界面和操作系统交互的逻辑;它们不应包含要在界面中显示的数据。将这些数据放入 ViewModel 中。
  • ViewModel 类可存储和管理与界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
  • ViewModel 是推荐使用的 Android 架构组件之一。
  • ViewModelProvider.Factory 是可用于创建 ViewModel 对象的接口。

下表比较了界面控制器与保存这些控制器数据的 ViewModel 实例:

界面控制器

ViewModel

您在此 Codelab 中创建的 ScoreFragment 便是界面控制器的一个示例。

ViewModel 的一个示例就是您在此 Codelab 中创建的 ScoreViewModel

不包含任何要在界面中显示的数据。

包含界面控制器在界面中显示的数据。

包含用于显示数据的代码以及用户事件代码,例如点击监听器。

包含数据处理的代码。

在每次配置更改期间销毁并重新创建。

仅当关联的界面控制器(对于 Activity 或 Activity 完成时)或 Fragment 分离时永久销毁。

包含视图。

绝不应包含对 Activity、Fragment 或视图的引用,因为它们在配置更改后仍然有效,但 ViewModel 会保留。

包含对相关 ViewModel 的引用。

不包含对关联界面控制器的任何引用。

Udacity 课程:

Android 开发者文档:

其他:

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

为避免在设备配置更改期间丢失数据,您应将应用数据保存在哪个类中?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

问题 2

ViewModel 绝不应包含对 Fragment、Activity 或视图的任何引用。判断正误。

  • 正确
  • 错误

问题 3

ViewModel 会在何时被销毁?

  • 当界面控制器在设备屏幕方向发生更改的情况下被销毁并重新创建时。
  • 在屏幕方向发生变化时。
  • 当关联的界面控制器完成(如果是 activity)或分离(如果是 fragment)时。
  • 当用户按返回按钮时。

问题 4

ViewModelFactory 接口的用途是什么?

  • 实例化 ViewModel 对象。
  • 在屏幕方向改变期间保留数据。
  • 刷新屏幕上显示的数据。
  • 在应用数据发生更改时收到通知。

开始学习下一课:5.2:LiveData 和 LiveData 观察者

如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页