前言

本章重点讲解 ViewModel 的基础使用和原理;
因为 ViewModel 的原理比较简单,本章结合 LiveData、DataBinding、Lifecycle 一起搞一个小 demo 来讲解 ViewModel 的原理和基础使用;
ViewModel 用来干什么?
- 一个容器,如果数据打包在 ViewModel 中,当配置发生变化的时候,这些数据是不会丢失的,保证数据的稳定性;
- 作为和界面 Controller 直接交互的最上层数据中心
- 和界面 Controller 直接交互、数据中心;Activity Fragment 获得 ViewModel 对象,并从中获取数据显示到界面;
- 最上层 ViewModel 中的数据格式未必是最底层格式,而是针对各个界面定制后的数据格式,ViewModel会向下层真正的数据中心取数据,并整理成上层格式;
- 在Activity被销毁(转屏、系统语言切换)的时候,ViewModel 并没有被销毁,因此重建的 Activity 可以直接拿到数据,无需重新初始化------相当于是修复了 Activity Fragment 重建时数据也会被重建的 bug(虽然不是bug)
- 可以在多个 Controller (Activity 和 Fragment 或者 多个 Fragment) 之间共享同一个 ViewModel,达到共享数据的结果,实际上就是为 Activity 和 Fragment 加了一个无额外操作成本的抽象的数据层;
- 只支持竖屏的App 还需要 ViewModel 吗?
- 需要,它可以让你的Activity 和 Fragment 以及多个 Fragment 之间共享数据
- 如果用 Compose 可以共享数据吗?
- 可以在多个 Compose 组件间共享数据
- 如果你的App只支持竖屏,没有使用 Fragment,也没有自定义View 也不用 Compose 还需要ViewModel吗?
- 不需要了
- 只支持竖屏的App 还需要 ViewModel 吗?
- 减轻 Activity 的负担,让它不要什么都干;
基础使用篇
使用很简单,就是继承 ViewModel
kotlin
import androidx.lifecycle.ViewModel
class MyViewModel : ViewModel() {
var number : Int = 0;
}
然后 Activity 中创建它的实例
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 不能直接实例化,因为如果能这样写,系统不可控了
// myViewModel = MyViewModel()
myViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(MyViewModel::class.java)
text_number.text = "${myViewModel.number}"
// 点击事件 lambda
btn_plus.setOnClickListener {
text_number.text = "${++myViewModel.number}"
}
}
}
PS: 不能使用 new
的方式,这样写,系统就不可控了;应该使用 ViewModel 提供者 ViewModeProvider 来创建;
运行之后,我们改变数值之后,旋转屏幕,可以看到,界面上的数值并不会被重置为 0 ;
组合使用
我们来搞一个小 demo,组合使用 LiveData + DataBinding + ViewModel + Lifecycle
我们就来搞一个『拨号键盘』点击键盘显示对应的数字,以及调用系统的打电话能力;
我们先来创建 ViewModel,PhoneViewModel
typescript
public class PhoneViewModel extends AndroidViewModel {
// 结合 LiveData 实现感应能力;
private MutableLiveData<String> phoneInfo;
// 上下文环境
private Context context;
public MainViewModel(Application application) {
super(application);
context = application;
}
// 提供数据接口 给布局用
public MutableLiveData<String> getPhoneInfo() {
if (phoneInfo == null) {
phoneInfo = new MutableLiveData<>();
// 设置默认值
phoneInfo.setValue("");
}
return phoneInfo;
}
/**
* 输入
*
* @param number
*/
public void appendNumber(String number) {
phoneInfo.setValue(phoneInfo.getValue() + number);
}
/**
* 删除
*/
public void backspaceNumber() {
int length = phoneInfo.getValue().length();
if (length > 0) {
phoneInfo.setValue(phoneInfo.getValue().substring(0, phoneInfo.getValue().length() - 1));
}
}
/**
* 清空
*/
public void clear() {
phoneInfo.setValue("");
}
/**
* 拨打
*/
public void callPhone() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:" + phoneInfo.getValue()));
// 非 Activity 启动拨号 或者是 非 Activity 启动任何的 startActivity 都会奔溃
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
这里我们使用继承 AndroidViewModel,AndroidViewModel 需要我们复写构造方法,传入 Application,这样我们就可以在 PhoneViewModel 中拿到 Context;
scala
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
@NonNull
public <T extends Application> T getApplication() {
return (T) mApplication;
}
}
接下来我们来创建 xml 布局文件,并结合 DataBinding 关联 PhoneViewModel
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="vm"
type="com.llc.jetpack.PhoneViewModel" />
</data>
<!-- UI绘制区域 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@drawable/phone2_bg">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1" />
<!-- 电话号码 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/activity_phone_tv"
android:gravity="center"
android:text="@{vm.phoneInfo}"
android:textStyle="bold" />
<!-- 表格布局 -->
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="16dip">
<!-- 第一列 -->
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone1"
android:onClick="@{()->vm.appendNumber(String.valueOf(1))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone2"
android:onClick="@{()->vm.appendNumber(String.valueOf(2))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone3"
android:onClick="@{()->vm.appendNumber(String.valueOf(3))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
</TableRow>
<!-- 第二列 -->
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone4"
android:onClick="@{()->vm.appendNumber(String.valueOf(4))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone5"
android:onClick="@{()->vm.appendNumber(String.valueOf(5))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone6"
android:onClick="@{()->vm.appendNumber(String.valueOf(6))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
</TableRow>
<!-- 第3列 -->
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone7"
android:onClick="@{()->vm.appendNumber(String.valueOf(7))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone8"
android:onClick="@{()->vm.appendNumber(String.valueOf(8))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone9"
android:onClick="@{()->vm.appendNumber(String.valueOf(9))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
</TableRow>
<!-- 第4列 -->
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phonexin"
android:onClick="@{()->vm.appendNumber(@string/phonexin)}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phone0"
android:onClick="@{()->vm.appendNumber(String.valueOf(0))}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/phonejin"
android:onClick="@{()->vm.appendNumber(@string/phonejin)}"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_number" />
</TableRow>
<!-- 第5列 -->
<TableRow
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dip">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<!-- 清空 -->
<Button
android:layout_width="40dp"
android:layout_height="40dp"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_min"
android:layout_gravity="center"
android:onClick="@{()->vm.clear()}"
android:layout_margin="6dip" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<!-- 拨打 -->
<ImageView
android:layout_width="46dip"
android:layout_height="46dip"
android:src="@drawable/phone_selector_call"
android:onClick="@{()->vm.callPhone()}"
android:layout_gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<!-- 删除一个字符 -->
<Button
android:layout_width="60dp"
android:layout_height="wrap_content"
android:textSize="@dimen/activity_phone_bt"
android:background="@drawable/phone_selector_backspace"
android:layout_gravity="center"
android:onClick="@{()->vm.backspaceNumber()}"/>
</LinearLayout>
</TableRow>
</TableLayout>
</LinearLayout>
</layout>
可以看到一个小 tips,在 DataBinding 下,我们的每个控件其实是不需要声明 id 的;
接下来,我们在 MainActivity 中进行绑定;
scala
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding dataBinding; // DataBinding 初始化
private PhoneViewModel phoneViewModel; // PhoneViewModel 初始化
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// extends ViewModel
// phoneViewModel = new ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(PhoneViewModel.class);
// extends AndroidViewModel
phoneViewModel = new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(PhoneViewModel.class);
// 让 ViewModel 和 DataBinding 建立关联
dataBinding.setVm(phoneViewModel);
// 让 LiveData 和 DataBinding 建立关联
dataBinding.setLifecycleOwner(this);
}
}
因为 PhoneViewModel 继承了 AndroidViewModel,所以 PhoneViewModel 实例的创建要改成
csharp
new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication()))
.get(PhoneViewModel.class)
看到这里的时候,有人就要提出疑问了,为什么 xml 中的 TextView 的 android:text="@{vm.phoneInfo}" 就可以渲染数据,而不用在 Activity 中调用 phoneInfo.observe 方法,在 onChange 回调中调用 textView.setText 方法?
上一章的 DataBinding 源码分析的时候,我们可以猜测下,应该是 ActivityMainBindingImpl 中帮我们生成了 getValue 的调用,以及 setText 的逻辑;

核心代码是下面这两行,让 DataBinding 和 LiveData 以及 ViewModel 组合在一起,就想 ViewPage 和 TabLayout 组合一样;
kotlin
// 让 ViewModel 和 DataBinding 建立关联
dataBinding.setVm(phoneViewModel);
// 让 LiveData 和 DataBinding 建立关联
dataBinding.setLifecycleOwner(this);
这样,我们就构建了一个数据驱动UI的典型小 demo;
源码篇
一共使用了三个参数 this、ViewModelProvider.NewInstanceFactory()、PhoneViewModel.class
this
csharp
phoneViewModel = new ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(PhoneViewModel.class);
我们进入这个构造方法看下:
less
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
这里调用了 owner.getViewModelStore() 方法,owner 就是我们传入的 this 当前 Activity;我们进入这个 getViewModelStore 方法看下:
ini
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
这里就是创建 new ViewModelStore() ViewModelStore
typescript
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}
可以看到,这里面持有了一个 HashMap 用来存储 ViewModel 的具体实现;
本质上就是我们创建的 Activity 就是这个 ViewModelStore 用来存储和这个 Activity 关联的 ViewModel;
Factory
Factory 用来反射创建我们要创建的 ViewModel 实例;我们进入这个 NewInstanceFactory 看下:
typescript
public static class NewInstanceFactory implements Factory {
private static NewInstanceFactory sInstance;
@NonNull
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}
@SuppressWarnings("ClassNewInstance")
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
try {
return modelClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
}
}
}
最终通过 create 方法调用到我们传入的 class 对象的 newInstance() 方法来创建实例;
ViewModel.class
我们进入这个 get 方法看下:
less
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
用 class 对象的 CanonicalName 作为 key;
less
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
先判断 ViewModelStore 中是否有,没有则调用 Factory 的 create 方法进行创建,然后保存到 ViewModeStore 中;
为什么横竖屏切换的时候,数据不会销毁
由 AMS 可以知道,当发生横竖屏切换的时候,最终会回调到 Activity.java 中的 retainNonConfigurationInstances
方法;
我们进入这个方法看下:
ini
NonConfigurationInstances retainNonConfigurationInstances() {
// 核心逻辑 1
Object activity = onRetainNonConfigurationInstance();
HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
mFragments.doLoaderStart();
mFragments.doLoaderStop(true);
ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();
if (activity == null && children == null && fragments == null && loaders == null
&& mVoiceInteractor == null) {
return null;
}
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.activity = activity;
nci.children = children;
nci.fragments = fragments;
nci.loaders = loaders;
if (mVoiceInteractor != null) {
mVoiceInteractor.retainInstance();
nci.voiceInteractor = mVoiceInteractor;
}
return nci;
}
核心逻辑1:onRetainNonConfigurationInstance(); 我们进入这个方法看下:
typescript
public Object onRetainNonConfigurationInstance() {
return null;
}
Activity.java 中是一个空实现,说明是其子类进行了实现,我们进入子类看下,点击左边的向下剪头;
ini
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();
ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
// 核心逻辑1 获取上一次的 NonConfigurationInstances 并获取其中的 ViewModelStore
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}
if (viewModelStore == null && custom == null) {
return null;
}
// 核心逻辑2 将 viewModelStore 保存到 NonConfigurationInstances 中
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
核心逻辑1 获取上一次的 NonConfigurationInstances 并获取其中的 ViewModelStore,横屏切换竖屏,获取的是竖屏时候的 NonConfigurationInstances 并获取其中的 ViewModelStore 来恢复竖屏的数据;
核心逻辑2 这也就能对上,getViewModelStore 方法中,为什么会优先从 NonConfigurationInstances 中获取;
ViewModel 生命周期

可以看到,只有在 Activity 执行了 onDestory,ViewModel 的数据才会清空;
屏幕旋转 发生了 onDestroy 和 onCreate 为什么 ViewModel 的数据没有销毁
ComponentActivity 中也会通过 Lifecycle 监听生命周期;
less
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
这里有个 isChangingConfigurations() 的判断,如果没有发生屏幕旋转,则在 Activity 销毁的时候清空 ViewModelStore,如果发生了旋转,则不会清空;
好了,ViewModel 就讲到这里吧~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~