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 应用架构。
  • 如何在应用中使用 Lifecycle、ViewModel 和 ViewModelFactory 类。
  • 如何在设备配置更改时保留界面数据。
  • 什么是工厂方法设计模式以及如何使用它。
  • 如何使用接口 ViewModelProvider.Factory 创建 ViewModel 对象。

您将执行的操作

  • 向应用添加 ViewModel,以保存应用的数据,使数据在发生配置更改后继续留存。
  • 使用 ViewModelFactory 和工厂方法设计模式来实例化具有构造函数参数的 ViewModel 对象。

在第 5 课的 Codelab 中,您将从起始代码开始开发 GuessTheWord 应用。GuessTheWord 是一款双人猜字谜游戏,玩家在游戏中可以协作以获得最高得分。

第一位玩家查看应用中的单词,然后依次表演每个单词,确保不让第二位玩家看到单词。第二位玩家尝试猜出该单词。

要玩这个游戏,第一个玩家需要在设备上打开应用,然后会看到一个字词,例如“吉他”,如下面的屏幕截图所示。

第一位玩家表演该字词,但要小心不要实际说出该字词。

  • 当第二位玩家正确猜出单词后,第一位玩家会按 Got It 按钮,这会将计数增加 1,并显示下一个单词。
  • 如果第二位玩家猜不出单词,第一位玩家可以按跳过按钮,这样一来,计数器会减 1,并跳到下一个单词。
  • 如需结束游戏,请按 End Game 按钮。(此功能未包含在本系列第一个 Codelab 的起始代码中。)

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

第 1 步:开始使用

  1. 下载 GuessTheWord 起始代码,并在 Android Studio 中打开该项目。
  2. 在 Android 设备或模拟器上运行应用。
  3. 点按相应按钮。请注意,点按 Skip 按钮后会显示下一个单词,并且得分会减 1;点按 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(用于得分界面)

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

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

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

screens/score/ScoreFragment.kt

ScoreFragment 是游戏的最终界面,用于显示玩家的最终得分。在此 Codelab 中,您将添加用于显示此界面和最终得分的实现。

res/navigation/main_navigation.xml

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

  • 用户可以从标题 fragment 导航到游戏 fragment。
  • 用户可以从游戏 fragment 导航到得分 fragment。
  • 用户可以从得分 fragment 导航回游戏 fragment。

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

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

应用中的问题:

  • 在配置更改期间(例如设备的屏幕方向发生变化时或应用关闭并重新启动时),起始应用不会保存和恢复应用状态。
    您可以使用 onSaveInstanceState() 回调来解决此问题。不过,使用 onSaveInstanceState() 方法需要编写额外的代码将状态保存在一个软件包中,并实现相应逻辑来检索该状态。而且,可以存储的数据量极少。
  • 当用户点按 End Game 按钮时,游戏界面不会导航到得分界面。

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

应用架构

应用架构是一种设计应用类及其之间关系的方式,可确保代码井然有序、在特定场景中表现出色,并且易于使用。在这组包含四个 Codelab 的课程中,您对 GuessTheWord 应用所做的改进遵循了 Android 应用架构指南,并且使用了 Android 架构组件。Android 应用架构类似于 MVVM(模型-视图-视图模型)架构模式。

GuessTheWord 应用遵循分离关注点设计原则,并划分为多个类,每个类负责处理一个单独的关注点。在本课的第一个 Codelab 中,您将使用的类包括界面控制器、ViewModel 和 ViewModelFactory。

界面控制器

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

在 GuessTheWord 起始代码中,界面控制器是三个 fragment:GameFragment、ScoreFragment, 和 TitleFragment。遵循“关注点分离”的设计原则,GameFragment 仅负责将游戏元素绘制到屏幕上并了解用户何时点按按钮,除此之外不负责任何其他操作。当用户点按某个按钮时,此信息会传递给 GameViewModel。

ViewModel

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

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

ViewModelFactory

ViewModelFactory 可实例化 ViewModel 对象,无论是否带有构造函数形参。

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

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

在此任务中,您将向应用添加第一个 ViewModel,即 GameFragment 的 GameViewModel。您还将了解 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 的运作方式:

  • 如果存在现有的 ViewModel,ViewModelProvider 会返回该对象;如果不存在,则会创建一个新的 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 进行过滤。点按设备或模拟器上的播放按钮。游戏界面随即打开。

    如 Logcat 中所示,GameFragment 的 onCreateView() 方法会调用 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. 退出游戏或退出游戏 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 之前和之后,初始应用中 GameFragment 界面数据的处理方式:ViewModel

  • 添加 ViewModel 之前:
    当应用经历配置变更(例如屏幕旋转)时,游戏 fragment 会被销毁并重新创建。数据会丢失。
  • 添加 ViewModel 并将游戏 fragment 的界面数据移入 ViewModel 后:
    fragment 需要显示的所有数据现在都位于 ViewModel 中。当应用经历配置更改时,ViewModel 会继续存在,并且数据会保留。

在此任务中,您将应用的界面数据以及处理数据的方法移至 GameViewModel 类。这样做是为了在配置更改期间保留数据。

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

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

  1. 移动 word、score 和 wordList 数据字段。确保 word 和 score 不是 private。

    请勿移动绑定变量 GameFragmentBinding,因为它包含对视图的引用。此变量用于扩充布局、设置点击监听器以及在屏幕上显示数据,这些都是 fragment 的职责。
  2. 移动了 resetList() 和 nextWord() 方法。这些方法决定了要在屏幕上显示哪些字词。
  3. 在 onCreateView() 方法内,将对 resetList() 和 nextWord() 的方法调用移至 GameViewModel 的 init 块。

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

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

目前,请在两个位置放置相同的方法:

  1. 将 GameFragment 中的 onSkip() 和 onCorrect() 方法复制到 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 无法再访问该方法。

    在 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 中,因此在配置更改期间会保留。

在此任务中,您将实现 End Game 按钮的点击监听器。

  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 类。此类将成为得分片段的 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 中,为 ScoreViewModel 和 ScoreViewModelFactory 创建类变量。
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. 在 ScoreFragment 的 onCreateView() 中,初始化 binding 变量后,初始化 viewModelFactory。使用 ScoreViewModelFactory。将参数 bundle 中的最终得分作为构造函数参数传递给 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 应用架构准则建议将具有不同责任的类分离。
  • 界面控制器是基于界面的类,例如 Activity 或 Fragment。界面控制器应仅包含处理界面和操作系统交互的逻辑;它们不应包含界面中要显示的数据。将这些数据放入 ViewModel 中。
  • ViewModel 类负责存储和管理与界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
  • ViewModel 是推荐使用的 Android 架构组件之一。
  • ViewModelProvider.Factory 是一个可用于创建 ViewModel 对象的接口。

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

界面控制器

ViewModel

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

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

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

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

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

包含用于数据处理的代码。

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

仅当关联的界面控制器永久消失时销毁 - 对于 activity,是在 activity 完成时;对于 fragment,是在 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 着陆页。