Jetpack 之 ViewModel

本文深入解析Jetpack ViewModel组件,探讨其在MVVM架构中的角色,包括数据保存、生命周期管理及跨组件数据共享等功能,帮助开发者更好地理解并运用此组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Jetpack 系列第三篇,这次回顾 ViewModel,ViewModel 作为 MVVM 架构中的 VM 层,具有自己的生命周期,且生命周期贯穿整个 Activity 或 Fragment,相较于之前 MVP 的 Presenter,它的存活时间更长,所以作为界面数据的持有者和控制者会更有优势 。

一、ViewModel 有什么用

  1. 首先,ViewModel 可以将视图层的逻辑剥离出来,所有与视图相关的数据应该存在于 ViewModel 中,而不是 Activity 或 Fragment,这一点它与 Presenter 作用类似,皆致力于减少 View 层逻辑;
  2. 相较于之前 MVP 的 Presenter,它的生命周期更长,在 Activity 因配置改变而重建时,ViewModel 的数据仍可得到保存。
  3. 可指定作用域,实现 Activity 与 Fragment 之间或 Fragment  与 Fragment  之间数据共享。

二、ViewModel 怎么用

ViewModel 作为一个抽象类,当我们自己定义一个类继承自它的时候,由于它也是一个普通的类,所以也可以用 new 的方式创建实例,但是这样做的话就失去它的意义了,在 Activity 重建时,ViewModel 也会重新创建。

package com.qinshou.jetpackdemo.viewmodel

import androidx.lifecycle.ViewModel

class TestViewModel : ViewModel() {
    var i = 0
}
package com.qinshou.jetpackdemo.viewmodel

import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.qinshou.jetpackdemo.R

class ViewModelActivity : AppCompatActivity() {
    companion object {
        const val TAG = "ViewModelActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_viewmodel)
        val testViewModel = TestViewModel()
        Log.i(TAG, "testViewModel--->$testViewModel")
        findViewById<Button>(R.id.btn_set_value).setOnClickListener {
            testViewModel.i++
        }
        findViewById<Button>(R.id.btn_get_value).setOnClickListener {
            Toast.makeText(this, "Value of view model is ${testViewModel.i}", Toast.LENGTH_SHORT).show()
        }
    }
}

直接 new 创建 ViewModel 实例,然后横竖屏切换一下,可以看到 ViewModel 不是同一个实例,而且 ViewModel 中持有的数据也被重新初始化了:

cde404f330684268b5fb5f1647a2e17c.png

67eca56675e64013b809ca4a9abfec04.gif

正确的做法应该是使用 ViewModelProvider 去创建 ViewModel,我们将创建 ViewModel 实例的方式修改一下,然后重新运行:

val testViewModel = ViewModelProvider(this).get(TestViewModel::class.java)

e22ed497743b42b9b8afbceecb62cd03.png

 760ac4ec01a347cba62ed86f66a7bb4b.gif

 可以看到横竖屏切换后,ViewModel 内存地址不变,而且持有的数据也得以保存。那么为什么通过 ViewModelProvider 就可以保证这样的效果呢,小追一下源码。

首先 ViewModelProvider 有三个构造方法:

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }


    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }


    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }

这三个构造方法最后走到了同一个地方,通常情况下我们使用只传一个 ViewModelStoreOwner 参数的构造方法即可,最后它会保存 Factory 和 ViewModelStore 实例。在默认实现中,这两个实例由 ComponentActivity 或者 Fragment 去创建(这里仅贴了 ComponentActivity 中的代码):

    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
    }
    public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mDefaultFactory == null) {
            mDefaultFactory = new SavedStateViewModelFactory(
                    getApplication(),
                    this,
                    getIntent() != null ? getIntent().getExtras() : null);
        }
        return mDefaultFactory;
    }

然后我们是通过 ViewModelProvider 的 get() 方法获取 ViewModel:

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

该方法将 ViewModel 的类名加上 DEFAULT_KEY 前缀作为 key 再次调用了 get 方法重载:

    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 中获取 ViewModel 实例,有就直接返回,没有的话就通过 Factory 创建然后放到 ViewModelStore 中再返回,相当于一个缓存机制,而获取和存储 ViewModel 的是在 ViewModelStore 中,那再来看看它是如何存储的:

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();
    }
}

可以看到 ViewModelStore 中其实是用一个 HashMap 去存储 ViewModel 实例的,那它到底是怎么保证 Activity 在异常销毁时保证 ViewModel 不变的呢?是不是只要在 Activity 异常销毁时,保存 ViewModelStore 的实例,重新创建 Activity 时再恢复就好了呢?事实上就是这样:

    public final Object onRetainNonConfigurationInstance() {
        // Maintain backward compatibility.
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so see if there was an existing
            // ViewModelStore from our last NonConfigurationInstance
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                viewModelStore = nc.viewModelStore;
            }
        }

        if (viewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }

在 Activity 异常销毁时会通过调用 onRetainNonConfigurationInstance(),将 ViewModelStore 保存在 NonConfigurationInstances 对象中,而刚才创建 ViewModelStore 的代码中,首先会从最后保存 NonConfigurationInstances 对象中获取,如果之前有保存,就直接拿来使用,所以系统内部就已经帮我们做好了异常销毁时的状态保存了。

三、ViewModel 作用域

关于这一点,其实我们通过刚才的源码知道 ViewModel 是如何创建出来的,那它作用域的问题就一目了然了。因为 ViewModel 其实是通过 ViewModelStore 对象获取的,而 ViewModelStore 是在创建 ViewModelProvider 对象时赋的值。那么在 Fragment 中,如果我们传递的 ViewModelStoreOwner 是 this,那就是它自己的 ViewModelStore,如果传递是 requireActivity(),那使用的就是宿主 Activity 的 ViewModelStore,这样就能达到多个子 Fragment 中共享数据的目的了。小做演示:

class ViewModelFragment1 : Fragment() {
    companion object {
        const val TAG = "ViewModelFragment1"
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return LayoutInflater.from(context).inflate(R.layout.fragment_viewmodel, null, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val testViewModel = ViewModelProvider(this).get(TestViewModel::class.java)
        Log.i(TAG, "testViewModel--->$testViewModel")
        view.findViewById<TextView>(R.id.text_view).text = "Value of view model is ${testViewModel.i}"
    }
}
class ViewModelFragment2 : Fragment() {
    companion object {
        const val TAG = "ViewModelFragment1"
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return LayoutInflater.from(context).inflate(R.layout.fragment_viewmodel, null, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val testViewModel = ViewModelProvider(requireActivity()).get(TestViewModel::class.java)
        Log.i(TAG, "testViewModel--->$testViewModel")
        view.findViewById<TextView>(R.id.text_view).text = "Value of view model is ${testViewModel.i}"
    }
}

这两个 Fragment 代码除了创建 ViewModelProvider 传递的参数不一样,其他都一样,用了一个文本框去显示 ViewModel 中的数据,然后在刚才的 Activity 中增加两个按钮分别显示这两个 Fragment(代码太简单就不贴了),效果如下:

bb4032bf39114b37b188fba3ec0d0b98.png

56c5866dbcc849ccb17ea6d27dc9928b.gif

可以看到 ViewModelFragment1 中的 ViewModel 对象内存地址与宿主 Activity 不一样,里面的数据值也不一样,而 ViewModelFragment2 是通过宿主 Activity 去获取的 ViewModelProvider 从而获取的 ViewModel 对象,所以内存地址和数据值都跟宿主 Activity 是一样的。

四、总结

ViewModel 作为 MVVM 中的重要一环,相当于 Presenter 的升级版,结合 LiveData 使用,可以节省很多之前需要注意的操作。ViewModel 的使用也不难,在了解它的特性是如何去实现后,相信运用起来会更加得心应手。

五、Demo 地址

禽兽先生/JetpackDemo-ViewModelhttps://gitee.com/MrQinshou/jetpack-demo/tree/master/app/src/main/java/com/qinshou/jetpackdemo/viewmodel

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值