- 原文地址:
- 原文作者:
引言
大约两年前,我在传授课程,这是一门让零编程基础的学生学习如何编写第一个Android应用程序的课程。作为课程的一部分,学生们将开发一个非常简单的名为的应用程序。
Court-Counter是一个非常简单的应用程序,只有一个界面,里面提供了一些按钮用于修改篮球比赛的比分。学生们最终完成的应用程序有都存在一个bug: 如果旋转手机的屏幕,应用程序界面上的当前比分将莫名其妙地丢失。
这是怎么回事?旋转设备的屏幕是应用程序在其生命周期中可能经历的一些configuration changes中的一种,其它还包括等改设备的语言等。 所有这些configuration changes都会导致Activity被销毁并重新创建。 Android系统的这种机制可以让我们做一些有趣的事情,比如在设备旋转的时候使用横向布局,但它却可能会让Android开发新手头疼不已。
在2017年Google I / O大会上,Android Framework团队推出了一套新的,其中的一个组件ViewModel就是用来处理这个屏幕的旋转问题。
ViewModel 类旨在以一种能够感知生命周期的方式来保存和管理与UI相关的数据,这使得数据能够在configuration changes(如屏幕旋转)的时候不会丢失。
这篇文章是探索ViewModel细节的系列文章中的第一篇。 在这篇文章中,我会:
- 解释ViewModel所能满足的基本需求
- 使用ViewModel来重构Court-Counter的代码从而解决屏幕旋转问题
- 深入研究ViewModel和UI组件之间的关系
根本的问题
问题的根本原因在于Activity的生命周期有很多不同的状态,并且由于configuration changes,一个Activity可能会多次经历这些不同的状态。
当一个Activity正在经历所有的这些状态时,您可能还需要在内存中保存一些UI的临时数据。我将UI的临时数据定义为UI所需的数据。它包括用户输入的数据,应用在运行时生成的数据或者是从数据库加载的数据。这些数据可能是位图图像,RecyclerView所需的对象列表,或者是本文中提到的篮球得分。
ViewModel出现之前,在configuration changes的时候您可能会使用onRetainNonConfigurationInstance方法来保存此数据,并使用getLastNonConfigurationInstance方法来取出这些数据。但是如果你的数据不需要知道Activity正处于处生命周期的哪种状态,它会不会无限膨胀?如果这些数据不是像Activity的变量scoreTeamA那样,与Activity的生命周期紧密相,而是存储在Activity之外的其他位置,该怎么办? 这正是ViewModel类存在的意义。
在下面的图表中,您可以看到一个Activity的生命周期,该Activity经历了一次屏幕旋转,然后最终被finish。ViewModel的生命周期显示在相对应的Activity生命周期的旁边。 请注意,ViewModels可以很方便的用在Fragment和Activity里,我将称其为UI controllers。本文重点介绍的是在Activity里如何使用ViewMode。
从你第一次请求ViewModel(通常在onCreate Activity中)开始到Activity最终被销毁,ViewModel会一直存在。 onCreate可能会在Activity的生命周期中多次调用,例如设备的屏幕发生旋转,但ViewModel还是同一个ViewModel。一个很简单的示例
ViewModel的使用可以分为一下三个步骤:
- 创建一个继承ViewModel的类,将数据从UI controllers中分离出来。
- 将ViewModel和你的UI controllers关联起来。
- 在您的UI controllers中使用ViewModel。
Step 1: 创建一个ViewModel类
一般来说,您需要为您应用中的每个界面创建一个ViewModel类。 这个ViewModel类将保存与界面相关的所有数据,并为存储的数据提供getter和setter方法。这样就将用户界面(在Activity和Fragment中实现)中需要显示的数据从UI controllers中分离出来,现在该数据位于ViewModel中。 所以,让我们为Court-Counter中的一个界面创建一个ViewModel类
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 controllers和ViewModel关联起来
你的UI controllers(这里是指Activity或Fragment)需要知道你的ViewModel,因为用户在与UI发生交互的时候需要显示数据和更新数据,例如按下按钮以增加Court-Counter计数器中的团队得分。 ViewModels不应该包含Activity、Fragment或Context的引用。此外,ViewModels还不应包含对UI controllers中的变量(如Views)的引用,因为这将创建对Context的间接引用。
不要在ViewModels里存储这些对象的原因是ViewModels是独立于你的UI controllers实例之外的- 如果你三次旋转一个Activity的屏幕方向,那么系统会创建三个不同的Activity实例,但你只有一个ViewModel。
考虑到这一点,我们需要实现这个UI controller 与 ViewModel之间关联。 您需要在UI controller中为您的ViewModel创建一个成员变量。 然后在onCreate中调用:
ViewModelProviders.of().get( .class)复制代码
在Court-Counter里是这样写的:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class); // Other setup code below...}复制代码
注意: “ViewModels中不应包含Context”这个原则有一个例外情况。 有时您可能需要Application context (而不是Activity context )以获取诸如系统服务之类的东西。将Application context存储在ViewModel中是可以的,因为Application context是与应用程序的生命周期相关联的。这与Activity context不同,后者与Activity的生命周期相关联。事实上,如果你需要一个Application context,你应该继承AndroidViewModel类,它是一个包含Application context的ViewModel。
Step 3: 在你的UI Controller中使用ViewModel
现在你可以在ViewModel中获取或更改UI中数据了。 下面是一个新的onCreate方法的示例:
// The finished onCreate method@Overrideprotected 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 ViewModelpublic void addOneForTeamA(View v) { mViewModel.scoreTeamA = mViewModel.scoreTeamA + 1; displayForTeamA(mViewModel.scoreTeamA);}复制代码
提示:ViewModel也可以与架构中的另一个组件LiveData一起工作,我不会在本系列中深入探讨。使用LiveData的好处在于它是可观察的:它可以在数据更改时触发UI的更新。您可以在了解更多关于LiveData的信息。
ViewModelProviders.of的原理
当MainActivity第一次调用ViewModelProviders.of方法的时候,它将创建一个新的ViewModel实例。 之后MainActivity里的onCreate()方法每一次被调用的时候,ViewModelProviders.of也同样会被调用,但是它将返回与MainActivity相关联的预先存在的ViewModel,这就是ViewModel可以保存数据的原因。
前提条件是你必须传入正确的UI controller来作为ViewModelProviders.of的第一个参数。 虽然你不应该在ViewModel中存储UI controller,但ViewModel类会使用您传入的UI controller作为第一个参数来跟踪ViewModel和UI controller之间的关联关系。
ViewModelProviders.of().get(ScoreViewModel.class);复制代码
ViewModelProviders.of使得你的应用可以打开同一Activity或Fragment的不同实例,但ViewModel中却保存着不同的信息。
我们可以把Court-Counter扩展一下,使它能记录和显示多场篮球比赛的分数。比赛以列表形式呈现,然后点击列表中的某一场比赛会打开一个看起来像我们当前的MainActivity的界面,这里我称之为GameScoreActivity。
对于您打开的每场比赛所对应的GameScoreActivity,如果在GameScoreActivity的onCreate方法将其与ViewModel关联起来,它将创建一个不同的ViewModel实例。如果旋转其中一个界面的屏幕,则保持与同一ViewModel的连接。
所有这些逻辑都是通过调用ViewModelProviders.of(Your UI controller)get(Your ViewModel.class)方法来完成的。 所以只要你传入一个UI控制器的正确实例,它就可以正常工作。
最后我想说:ViewModels真的很好,可以将填充数据到视图的逻辑从UI controller分离出来。这意味着,它并不是一种数据持久化和保存应用程序状态的解决方案。在下一篇文章中,我将研究Activity生命周期与ViewModels之间的交互,并将ViewModel与onSaveInstanceState进行比较。
结束语
在这篇文章中,我介绍了一些关于ViewModel类的基础知识。关键要点是:
- ViewModel的目标是以一种能够感知生命周期的方式来保存和管理与UI相关的数据,这使得数据能够在configuration changes(如屏幕旋转)的时候不会丢失。
- ViewModels实现了UI与数据的分离。
- 一般来说,如果您应用中的界面有临时数据,你应该为该界面上的数据创建一个单独的ViewModel。
- ViewModel的生命周期从首次创建UI controller与ViewModel的关联开始,直至UI controller被完全销毁。
- 切勿将UI controller或Context直接或间接的存储在ViewModel中,包括在ViewModel中存储View。直接或间接的引用UI controller违背了将UI与数据分离的目的,并可能导致内存泄漏。
- ViewModel对象通常会存储LiveData对象,您可以在了解更多信息。
- ViewModelProviders.of方法通过它的参数(传入的UI controller)来跟踪它与对应的UI controller之间的关联关系。