(译) ViewModel系列:一个简单的例子

原文链接

介绍

两年多前,我正在开发给初学者的 Android 课程;这是一个将学生从零编程基础带到他们完成第一个Android 应用程序的课程。作为课程的一部分,学生们构建了一个非常简单的一个名为 Court-Counter 的屏幕应用程序。

Court-Counter 是一个非常简单的应用程序,带有修改篮球得分的按钮。应用程序完成了但是有一个 bug ;如果你旋转手机,你的当前分数将莫名其妙地消失。

这是怎么回事?旋转设备是应用程序在其生命周期内可以进行的一些配置更改之一,包括键盘可用性和更改设备的语言。所有这些配置更改都会导致 Activity 被拆除并重新创建。

这种行为允许我们在设备旋转时使用横向方向特定布局。不幸的是,对于新的(有时候不是那么新的)工程师来说,他们可能会头疼。

在 Google I/O 2017 中,Android Framework 团队引入了一组新的架构组件,其中一个组件是确切处理这个屏幕转向问题的。

ViewModel 类旨在以感知生命周期的方式保存和管理 UI 相关数据。这允许数据在配置更改(如屏幕旋转)后继续存在。

这篇文章是探索 ViewModel 细节的系列文章中的第一篇。在这篇文章中,我将:

  • 解释 ViewModels 实现的基本需求
  • 通过更改 Court-Counter 代码以使用 ViewModel 来解决转向问题
  • 细看 ViewModel 和 UI Component 之间的联系

根本问题

潜在的挑战是 Android Activity 生命周期有很多状态,并且由于配置更改,单个 Activity 可能会多次循环通过这些不同的状态。

当 Activity 经历所有这些状态时,你可能会有需要保留在内存中的短时 UI 数据。我将把短时UI数据定义为 UI 所需的数据。示例包括用户输入的数据,运行时生成的数据或从数据库加载的数据。这些数据可以是位图图像,RecyclerView 所需的对象列表,或者在这种情况下是篮球得分。

以前,你可能已使用 onRetainNonConfigurationInstance 在配置更改期间保存此数据,并在另一端将其打开。但是,如果你的数据不需要知道或管理 Activity 所处的生命周期状态,那么它会不会令人愉快? Activity 中不再是有一个像 scoreTeamA 这样的变量,不再与 Activity 起伏不定的生命周期绑定起来,如果那些数据存储在 Activity 之外的其他地方会是怎么样?这就是 ViewModel 类的目的所在

在下图中,你可以看到 Activity 的生命周期,该 Acitivity 经历屏幕转向之后然后最终 Finished 了。 ViewModel 的生命周期显示在与之相关联的 Activity 生命周期旁边。请注意,ViewModel 可以很容易地与 Fragment 和 Activitiy 一起使用,我称之为 UI控制器。此示例重点关注 Activity。

ViewModel 从你第一次请求 ViewModel(通常在 onCreate the Activity中)到Activity finished 并 destroyed 之时就存在。在活动的生命周期中可以多次调用 onCreate,例如旋转应用程序时,但 ViewModel 始终存在。

一个简单的例子

设置和使用 ViewModel 有三个步骤:

  1. 通过创建扩展 ViewModel 的类,从 UI 控制器中分离出数据
  2. 设置 ViewModel 和 UI 控制器之间的通信
  3. 在 UI 控制器中使用 ViewModel

第一步: 创建 ViewModel 类

注意:要创建 ViewModel,首先需要添加正确的 lifecycle 依赖。看看这里怎么样。

通常,你将为应用中的每个屏幕创建一个 ViewModel 类。此 ViewModel 类将保存与屏幕关联的所有数据,并具有存储数据的 getter 和 setter。这将代码分离,以显示你的 Activity 和 Fragment 中实现的 UI,该数据现在位于 ViewModel 中。那么,让我们在 Court-Counter 中为一个页面创建一个 ViewModel 类:

1
2
3
4
5
6
7
public class ScoreViewModel extends ViewModel {
// 记录 A 队伍的分数
public int scoreTeamA = 0;

// 记录 B 队伍的分数
public int scoreTeamB = 0;
}

为简洁起见,我选择将数据存储为我的 ScoreViewModel.java 中的公共成员变量,但创建getter 和 setter 以更好地封装数据是一个好主意。

第二步: 将 UI 控制器与 ViewModel 相关联

你的 UI 控制器(也称为 Activity 或 Fragment)需要了解你的 ViewModel。这样你的 UI 控制器就可以在 UI 交互发生时显示数据并更新数据,例如按下按钮以增加团队在 Court-Counter 中的得分。

但是,ViewModel 不应该包含对 Activitiy,Fragment 或 Context 的引用。**此外,ViewModel 不应包含包含对 UI 控制器的引用的元素,例如 View,因为这将创建对 Context的间接引用。

你不应存储这些对象的原因是 ViewModel 比你的特定UI控制器实例存活时间更长 - 如果你将Activity 旋转三次,你刚刚创建了三个不同的 Activity 实例,但你只有一个ViewModel。

考虑到这一点,让我们创建这个 UI 控制器/ViewModel 关联。你需要在UI控制器中为ViewModel创建成员变量。然后在 onCreate 中,你应该调用:

1
ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)

在 Court-Counter 的情况下,这看起来像:

1
2
3
4
5
6
7
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);
// Other setup code below...
}

注意:ViewModel 中的无 context 规则有一个例外。有时你可能需要一个 Application context(而不是Activity context)来与系统服务一起使用。在 ViewModel 中存储应用程序 context 是可以的,因为应用程序 context 与应用程序生命周期相关联。这与Activity context 不同,后者与 Activity 生命周期相关联。实际上,如果你需要Application context,则应该扩展 AndroidViewModel,它只是一个包含 Application 引用的ViewModel。

第三步: 在你的 UI 控制器中使用 ViewModel

要访问或更改 UI 数据,你现在可以使用 ViewModel 中的数据。这是一个新的 onCreate 方法的示例,以及通过向团队 A 添加一个点来更新分数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 最终的 onCreate 方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);
displayForTeamA(mViewModel.scoreTeamA);
displayForTeamB(mViewModel.scoreTeamB);
}

// An example of both reading and writing to the ViewModel
public void addOneForTeamA(View v) {
mViewModel.scoreTeamA = mViewModel.scoreTeamA + 1;
displayForTeamA(mViewModel.scoreTeamA);
}

专业提示:ViewModel 也可以与另一个架构结构组件 LiveData 很好地配合,我将不会在本系列中深入探讨。使用 LiveData 的额外好处是它可以观察到:它可以在数据发生变化时触发UI更新。你可以在此处了解有关 LiveData 的更多信息。

细看 ViewModelsProviders.of

MainActivity 第一次调用 ViewModelProviders.of 方法时,它会创建一个新的 ViewModel 实例。当再次调用此方法时,只要调用 onCreate,就会发生此方法,它将返回与特定 Court-Counter MainActivity 关联的预先存在的 ViewModel。这是保留数据的原因。

仅当你将正确的 UI 控制器作为第一个参数传递时,此方法才有效。虽然你永远不应该在ViewModel 中存储 UI 控制器,但 ViewModel 类会使用你传入的 UI 控制器作为第一个参数来跟踪 ViewModel 和幕后 UI 控制器实例之间的关联。

1
ViewModelProviders.of(<THIS ARGUMENT>).get(ScoreViewModel.class);

这允许你拥有一个应用程序,可以打开相同 Activity 或 Fragment 的许多不同实例,但具有不同的 ViewModel 信息。让我们想象一下,如果我们扩展我们的 Court-Counter 示例来获得多个篮球比赛的分数。游戏以列表形式呈现,然后单击列表中的游戏将打开一个看起来像我们当前MainActivity 的屏幕,但我将其称为 GameScoreActivity。

对于你打开的每个不同的游戏屏幕,如果你在 onCreate 中关联 ViewModel 和GameScoreActivity,它将创建一个不同的 ViewModel 实例。如果旋转其中一个屏幕,则会保持与同一 ViewModel 的连接。

所有这些逻辑都是通过调用 ViewModelProviders.of(<您的UI控制器>)来完成的.get(<您的ViewModel>.class)。因此,只要你传入正确的 UI控制器实例,它就可以正常工作。

最后一点想法:ViewModel 非常适合将 UI 控制器代码与填充 UI 的数据分开。也就是说,它们并不能解决数据持久性和保存应用状态的问题。在下一篇文章中,我将探讨 Activity生命周期与 ViewModel 的微妙交互以及 ViewModel 与 onSaveInstanceState 的比较。

结论和进一步学习

在这篇文章中,我探讨了新 ViewModel 类的基础知识。关键要点是:

  • ViewModel 类旨在以生命周期意识的方式保存和管理 UI 相关数据。这允许数据在配置更改(如屏幕旋转)后继续存在。
  • ViewModel 将 UI 实现与应用程序的数据分开。
  • 通常,如果应用中的屏幕具有短时数据,则应为该页面的数据创建单独的 ViewModel。
  • ViewModel 的生命周期从首次创建关联的 UI 控制器时开始,直到完全销毁。
  • 切勿直接或间接地将 UI 控制器或 Context 存储在 ViewModel 中。这包括在 ViewModel中存储 View。对 UI 控制器的直接或间接引用会破坏将 UI 与数据分离的目的,并可能导致内存泄漏。
  • ViewModel 对象通常会存储 LiveData对象,你可以在此处了解更多信息。
  • ViewModelProviders.of 方法通过作为参数传入的UI控制器跟踪 ViewModel 与哪个 UI 控制器关联。

想要更多 ViewModel 相关的好东西?查看:

架构组件是根据你的反馈创建的。如果你对 ViewModel 或任何架构组件有任何疑问或意见,请查看我们的反馈页面。关于这个系列的问题或建议?发表评论!

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号