ViewModel一个简单例子

ViewModel: 一个简单例子

原文地址: 《ViewModels: A Simple Example》

[TOC]

介绍

两年多前, 我从事一档被称为“Android for Beginners”的面向零编程基础的学生并引导他们建立自己的第一个Android应用程序的课程。作为课程的一部分, 学生们需要构建一个简单的单页面应用——《篮球计分器》。

《篮球计分器》是一个非常直观的App。它的按钮用于修改篮球队的得分。尽管最终完成的app有一个bug, 如果你旋转手机屏幕,你当前的分数就会莫名其妙地消失。

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

这种行为引导我们应当进行诸如在设备旋转时使用横向特定布局的操作。不幸的是,这对于新手(哪怕有时候并不是那么新手)的工程师来说可能是个头疼的问题。

在2017年的Google I/O大会上, Android框架团队推出了一套新的架构组件,其中一个组件可以解决这一确切的旋转问题。

ViewModel类旨在以生命周期感知的方式保存和管理与UI相关的数据。它使得数据可以在配置变更(比如屏幕旋转)的情况下存活。

这篇文章是探索ViewModel来龙去脉的系列文章的第一篇。在这篇文章中,我将:

  • 解释ViewModels实现的基本需求
  • 使用ViewModel去改造《篮球计分器》的代码以解决旋转问题
  • 深入了解ViewModel和UI组件的联系

潜在的问题

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

当一个Activity经历了所有这些状态,您可能还需要将瞬态UI数据保存在内存中。我将瞬态UI数据定义为UI所需的数据,包括用户输入的数据,运行时生成的数据或从数据库加载的数据。这些数据可以是图片位图、用于RecyclerView的对象列表等,在这个例子中是篮球队分数。

显然, 你也许使用了onRetainNonConfigurationInstance在配置更改到重新加载它的期间保存瞬态UI数据。但是,如果您的数据不需要知道或管理Activity所处于的生命周期状态会不会隆起(swell)?与其在Activity内不声明像scoreTeamA这样的变量,而是异想天开地将其绑定到Activity生命周期,不如将这些数据存储在Activity之外的其他地方,该怎么办?这就是ViewModel类的目的。

在下面的图表中, 你可以看到Activity的生命周期在旋转过程中的状态流转直到finish。ViewModel的生命周期显示在关联的Activity的生命周期的旁边。请注意,这个ViewModels可以简单轻松地与UI控制器(Activities/Fragments)结合使用。

ViewModel存在于你第一次请求一个ViewModel(通常在Activity的onCreate中)直到Activity已经finished或者destoryed期间。onCreate在Activity的一次声明中也许会被地调用多次,比如当app发生旋转时,但是ViewModel在此期间一直存活着。

一个非常简单的例子

使用ViewModel的三个步骤:

  1. 通过创建一个继承自ViewModel的类来从你的UI控制器(Activity或Fragment)中分离出你的ViewModel
  2. 在你的UI控制器和ViewModel之间建立通讯
  3. 在你的UI控制器中使用ViewModel

第一步:创建一个ViewModel类

注意:为了创建ViewModel类,你第一步需要添加正确的lifecycle依赖. 看这里如何做。

通常情况下,你需要为你的app的每隔页面创建一个ViewModel类。这个ViewModel类会掌控和管理所有的与页面相关数据,并且提供get/set方法用于存取数据。这会将显示Activity的UI代码与您的用于显示UI的数据分离开(显示UI的数据现在位于ViewModel中)。那么,让我们为《篮球计分器》的那个页面创建一个ViewModel类:

1
2
3
4
5
6
7
public class ScoreViewModel extends ViewModel {
// Tracks the score for Team A
public int scoreTeamA = 0;

// Tracks the score for Team B
public int scoreTeamB = 0;
}

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

第二步: 关联UI控制器和ViewModel

你的UI控制器(即Activity或Fragment)需要知道你的ViewModel。这样在诸如“在《篮球计分器》中按下按钮去增加某一队分数”等UI交互发生时, UI控制器可以展示和更新数据。

但是,ViewModels不应保留对Activity,Fragment或Context的引用。此外,ViewModels不应包含那些拥有对UI控制器的引用的元素,例如Views,因为这将创建对Context的间接引用。

你不该保存这些对象的原因是, ViewModels的寿命超出了特定的UI控制实例之外——如果您将Activity旋转3次,则您刚刚创建了三个不同的Activity实例,但是只有一个ViewModel实例。

考虑到这一点,让我们来创建这个UI控制器/ViewModel关联。你想要为在UI控制器中的ViewModel创建一个成员变量。那么在onCreate中,你应该这样写:

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

对于《篮球计分器》,它应该像这样写:

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中没有上下文”的规则有一个例外: 有时您可能需要一个Application的Context(而不是Activity的Context)来与诸如系统服务之类的东西一起使用。那么,您可以将应用程序上下文储存在ViewModel中,因为应用程序上下文与应用程序生命周期相关联。这不同于与Activity生命周期相关的Activity的Context。实际上如果需要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
// The finished onCreate method
@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中第一次调用ViewModelsProviders.of方法时,一个ViewModel实例就创建完成了。当这个方法再次被调用时(即onCreate方法被再次调用),它会返回一个与确切的《篮球计分器》的MainActivity相关联的预先存在的ViewModel。这就是保存数据的原因。

仅当您传入正确的UI控制器作为第一个参数时, 此方法才有效。尽管你永远不应将UI控制器储存在ViewModel内,但ViewModel类确实会使用您传入的第一个参数(UI控制器)来跟踪ViewModel和UI控制器实力在后台之间的关联。

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

这使得您的app可以打开很多不同的Activity/Fragment实例,但是持有不同的ViewModel内容。让我们想象一下,如果我们拓展我们的《篮球计分器》例子——可以记录多场篮球赛的比分。所有的比赛都展示在一个列表页。点击列表中的其中一个比赛,就会打开一个像我们当前的MainActivity的页面,我们可以将它称作GameScoreActivity

为了你所打开的每场不同的比赛的积分页面,你可以在OnCreate中关联GameScoreActivity和ViewModel, 这样就会创建不同的ViewModel实例。如果你旋转了其中一个屏幕,那么与相同的ViewModel的关联关系会被保存下来。

通过调用ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)所有的这些逻辑都为你完成了。所以只要你传递了正确的UI控制器实例(Your UI controller),它就起作用了。

最后的思考: ViewModels十分巧妙地分离了用于展示UI的数据和UI控制器的代码。它们不能完全解决数据持久性和保存应用程序状态的问题。在下一篇文章里, 我会探索Activity生命周期与ViewModels的微妙交互,以及ViewModels与onSaveInstanceState的比较。

结论和进一步学习

在这篇文章里吗,我探索了ViewModel类的最基础用法。关键要点是:

  • ViewModel类旨在以生命周期感知的方式保存和管理与UI相关的数据。这使数据能够承受配置更改(例如屏幕旋转)的影响。
  • ViewModels将UI实施与应用数据分开。
  • 通常,如果您应用中的屏幕上有临时数据,则应为该屏幕上的数据创建一个单独的ViewModel。ViewModel的生命周期从首次创建关联的UI控制器开始,一直到完全销毁为止。
  • 切勿将UI控制器或Context直接或间接存储在ViewModel中。这包括将View存储在ViewModel中。直接或间接引用UI控制器会破坏将UI与数据分离的目的,并可能导致内存泄漏。
  • ViewModel对象通常会存储LiveData对象,您可以在此处了解更多信息。
  • ViewModelProviders.of方法跟踪通过作为参数传入的UI控制器与ViewModel关联的UI控制器。

想要了解更多ViewModel化的优势吗?请查阅:

架构组件的创建给予你们的反馈。如果您有什么问题和意见关于ViewModel或者任何其他的架构组件,请查看我们的意见反馈页面。有任何问题关于此系列文章吗?留下评论吧!

坚持原创技术分享,您的支持将鼓励我继续创作!
显示 Gitment 评论