百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Jetpack-VM再不懂你就out了(vm client)

liuian 2025-04-30 18:01 52 浏览

作者:heiyulong

授权原文:https://mp.weixin.qq.com/s/yZ7ZQYn7KjYxQiZT956NqQ

前言

ViewModel 作为 Jetpack 组件库组件之一,它的出现释放了 Activity/Fragment 管理数据的压力,ViewModel 经常会搭配 LiveData 一起用于 MVVM 的开发模式。

一、认识ViewModel

1、什么是 ViewModel?

  • ViewModel 具备宿主生命后期感知能力的数据存储与数据管理组件。
    • 使用 ViewModel 保存的数据,在页面因配置变更(如,发生屏幕旋转等配置更改后)导致页面销毁重建之后依然也是存在的;
    • ViewModel还处理Activity/Fragment与应用程序其余部分的通信(例如,调用业务逻辑类);
    • ViewModel的目的是获取并保留Activity或Fragment所需的信息。Activity或Fragment应能够观察ViewModel中的更改;
    • ViewModel通常通过LiveData或DataBinding公开此信息;
    • ViewModel的唯一责任是管理UI的数据。它永远都不应访问您的视图层次结构或保留对Activity或Fragment的引用。

2、ViewModel 的优势

页面配置更改数据不丢失

  • 当设备因配置更改导致 Activity/Fragment 重建,ViewModel 中的数据并不会因此而丢失,配合 LiveData 可以在页面重建后立马能收到最新保存的数据用以重新渲染页面。

Android的Activity的生命周期有很多状态,并且由于配置更改,单个Activity可能会在这些不同状态之间循环多次。参照下面Activity生命周期图:

Activity生命周期

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

如,用户旋转屏幕时,以前我们通过onSaveInstanceState()和onRestoreInstanceState()在配置更改期间保存或还原此数据。

用户旋转屏幕时如下图:当系统开始停止您的Activity时:

  • (1)它会调用onSaveInstanceState(),以便您可以指定要保存的其他状态数据,以防Activity必须重新创建实例。
  • (2)如果Activity被破坏并且必须重新创建相同的实例,则系统将(1)中定义的状态数据传递给onCreate()方法和onRestoreInstanceState()方法。

屏幕旋转:Avtivity生命周期流程

但是,如果您的数据不需要知道或管理“Activity”所处于的生命周期状态,或者不将其存储在Activity内该怎么做呢?这就是ViewModel类的目的

  • ViewModel 和 onSaveIntanceState 方法区别?
    • onSaveIntanceState 只能存储轻量级的 key-value 键值对数据,非配置变更导致的页面被回收时才会触发,此时数据存储在 ActivityRecord 中;
    • ViewModel 可以存放任意 Object 数据,因配置变更导致的页面被回收才有效。此时存在ActivityThread#ActivityClientRecord 中。

生命周期感应

  • 在 ViewModel 中难免会做一些网络请求或数据的处理,可以复写 onCleared() 方法,终止清理一些操作,释放内存。该方法在宿主 onDestroy 时被调用。

如下图:

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

您通常在系统首次调用 Activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 Activity 的整个生命周期内多次调用 onCreate(),如在旋转设备屏幕时。ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 Activity 完成并销毁。

数据共享

  • 对于单 Activity 对 Fragment 的页面,可以使用 ViewModel 实现页面之间的数据共享,实际上不同的 Activity也可以实现数据共享。

Activity 中的两个或更多 Fragment 需要相互通信是一种很常见的情况。

想象一下主从 Fragment 的常见情况,假设您有一个 Fragment,在该 Fragment 中,用户从列表中选择一项,还有另一个 Fragment,用于显示选定项的内容。

这种情况不太容易处理,因为这两个 Fragment 都需要定义某种接口描述,并且所有者 Activity 必须将两者绑定在一起。

此外,这两个 Fragment 都必须处理另一个 Fragment 尚未创建或不可见的情况。

可以使用 ViewModel 对象解决这一常见的难点。这两个 Fragment 可以使用其 Activity 范围共享 ViewModel 来处理此类通信,如以下示例代码所示:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        //!!!请注意:这两个 Fragment 都会检索包含它们的 Activity
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        //!!!请注意:这两个 Fragment 都会检索包含它们的 Activity
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), { item ->
           // Update the UI.
        });
    }
}   
  • 通过ViewModel在 Fragment 之间共享数据的优势:
    • 1、Activity 不需要执行任何操作,也不需要对此通信有任何了解。
    • 2、除了 SharedViewModel 约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。
    • 3、每个 Fragment 都有自己的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将继续工作而没有任何问题。

二、ViewModel 的使用

使用ViewModel的三个步骤:

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

LiveData , ViewModel 组件都是基于Lifecycle来实现的,使用 ViewModel 之前需要先添加依赖:

声明依赖项

dependencies {
        def lifecycle_version = "2.2.0"

        // ViewModel
        implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
}

1、创建一个ViewModel类

引用官方:一个非常简单的例子

  • 通常情况下,你需要为你的app的每个页面创建一个ViewModel类。
  • 这个ViewModel类会掌控和管理所有的与页面相关数据,并且提供get/set方法用于存取数据。
  • 这会将显示Activity的UI代码与您的用于显示UI的数据分离开(显示UI的数据现在位于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;
}

2、关联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中,你应该这样写:

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

引用最新版本2.2.0版本ViewModel你会发现,ViewModelProviders该类被弃用了。

参考一下lifecycle更新文档:

lifecycle更新文档

那么在onCreate中,你应该这样写:

 mViewModel = new ViewModelProvider(<Your UI controller>).get(<Your ViewModel>.class)
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   mViewModel = new ViewModelProvider(this).get(ScoreViewModel.class);
   // Other setup code below...
}

注意:“ViewModel中没有上下文”的规则有一个例外:

有时您可能需要一个Application的Context(而不是Activity的Context)来与诸如系统服务之类的东西一起使用。

那么,您可以将应用程序上下文储存在ViewModel中,因为应用程序上下文与应用程序生命周期相关联。

这不同于与Activity生命周期相关的Activity的Context。实际上如果需要Application的Context,则应该拓展AndroidViewModel,它只是一个包含Application引用的ViewModel。

3、在UI控制器中使用ViewModel

要访问或更改UI数据,现在可以在ViewModel中使用数据。这里有一个新的onCreate方法和一个为队伍A增加分数的更新方法的例子:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   mViewModel = new ViewModelProvider(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,DataBinding等等。

三、ViewModel复用的实现原理

上面说到,ViewModel 可以实现因配置变更导致页面销毁重建之后依然可以复用。

准确点来说,应该是 页面恢复重建前后获取到的是同一个 ViewModel 实例对象,以至于页面恢复重建后还能接着复用。

通过ViewModelProvider获取 ViewModel 实例的方式入手:

mViewModel = new ViewModelProvider(this).get(ScoreViewModel.class);
//指定factory
mViewModel = new ViewModelProvider(this,factory).get(ScoreViewModel.class);

ViewModelProvider 本质是从传递进去的 ViewModelStore 来获取实例。如果没有传递,则利用 factory 去创建一个新的,并存储到 ViewModelStore。

1、ViewModelProvider获取ViewModel实例get()方法源码分析

public class ViewModelProvider {

    private static final String DEFAULT_KEY =
            "androidx.lifecycle.ViewModelProvider.DefaultKey";
            
    private final Factory mFactory;
    private final ViewModelStore mViewModelStore;
    
    //1、        
    //ViewModelProvider 本质是从传递进去的 ViewModelStore 来获取实例。
    //如果没有传递,则利用 factory 去创建一个新的,并存储到 ViewModelStore。
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }
    
    //5、ViewModelStore()的由来,通过owner.getViewModelStore()?
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }
    
    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
    
    @NonNull
    @MainThread
    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");
        }
        //2、根据传递的modelClass 构建一个默认的Key
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
    
    @NonNull
    @MainThread
    //3、获取viewmodel实例时,也可以自行指定Key
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        //4、在ViewModelStore中拿到VM实例
        //ViewModelStore 一个真正用来存储ViewModel实例的集合。本质上是HashMap<String,ViewModel>
        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;
    }
}

2、ViewModelStore()的由来,通过owner.getViewModelStore()查看源码:

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
        
    static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
    }
    
    public ViewModelStore getViewModelStore() {
        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 (mViewModelStore == null) {
            //1、首先从`NonConfigurationInstances`来获取`ViewModelStore`实例对象
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            //2、如果不为空岂不是获取到的是同一个 ViewModel 实例对象,
            //以至于页面恢复重建后还能接着复用。
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                //3、`ViewModelStore`何时被存储到`NonConfigurationInstances`里面的?
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }
        
}

3、onRetainNonConfigurationInstance 源码

因系统原因页面被回收时,会触发onRetainNonConfigurationInstance方法,所以 viewModelStore 对象此时会被存储在NonConfigurationInstance 中。在页面恢复重建时,会再次把这个 NonConfigurationInstance 对象传递到新的Activity 中实现对象复用。

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
    /**
     * Retain all appropriate non-config state.  You can NOT
     * override this yourself!  Use a {@link androidx.lifecycle.ViewModel} if you want to
     * retain your own non config state.
     */
    @Override
    @Nullable
    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so see if there was an existing
            // ViewModelStore from our last
            //3、如果NonConfigurationInstance保存了viewModelStore,把它取出来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;
        //1、把viewModelStore放到NonConfigurationInstances中并返回
        nci.viewModelStore = viewModelStore;
        //2、当页面被销毁时ViewModelStore就被保存起来了。
        return nci;
    }
}

后台私信回复 1024 免费领取 SpringCloud、SpringBoot,微信小程序、Java面试、数据结构、算法等全套视频资料。

相关推荐

eino v0.4.5版本深度解析:接口类型处理优化与错误机制全面升级

近日,eino框架发布了v0.4.5版本,该版本在错误处理、类型安全、流处理机制以及代理配置注释等方面进行了多项优化与修复。本次更新共包含6个提交,涉及10个文件的修改,由2位贡献者共同完成。本文将详...

SpringBoot异常处理_springboot异常注解

在SpringBoot中,异常处理是构建健壮、可维护Web应用的关键部分。良好的异常处理机制可以统一返回格式、提升用户体验、便于调试和监控。以下是SpringBoot中处理异常的完整指...

Jenkins运维之路(Jenkins流水线改造Day02-1-容器项目)

这回对线上容器服务器的流水线进行了一定的改造来满足目前线上的需求,还是会将所有的自动化脚本都放置到代码库中统一管理,我感觉一章不一定写的完,所以先给标题加了个-1,话不多说开干1.本次流水线的流程设计...

告别宕机!零基础搭建服务器监控告警系统!小白也能学会!

前言本文将带你从零开始,一步步搭建一个完整的服务器指标监控与邮件告警系统,使用的技术栈均为业界主流、稳定可靠的开源工具:Prometheus:云原生时代的监控王者,擅长指标采集与告警规则定义Node_...

httprunner实战接口测试笔记,拿走不谢

每天进步一点点,关注我们哦,每天分享测试技术文章本文章出自【码同学软件测试】码同学公众号:自动化软件测试码同学抖音号:小码哥聊软件测试01开始安装跟创建项目pipinstallhttprunne...

基于JMeter的性能压测平台实现_jmeter压测方案

这篇文章已经是两年前写的,短短两年时间,JMeter开源应用技术的发展已经是翻天覆地,最初由github开源项目zyanycall/stressTestPlatform形成的这款测试工具也开始慢...

12K+ Star!新一代的开源持续测试工具!

大家好,我是Java陈序员。在企业软件研发的持续交付流程中,测试环节往往是影响效率的关键瓶颈,用例管理混乱、接口调试复杂、团队协作不畅、与DevOps流程脱节等问题都能影响软件交付。今天,给大家...

Spring Boot3 中分库分表之后如何合并查询

在当今互联网应用飞速发展的时代,数据量呈爆发式增长。对于互联网软件开发人员而言,如何高效管理和查询海量数据成为了一项关键挑战。分库分表技术应运而生,它能有效缓解单库单表数据量过大带来的性能瓶颈。而在...

离线在docker镜像方式部署ragflow0.17.2

经常项目上会出现不能连外网的情况,要怎么使用ragflow镜像部署呢,这里提供详细的步骤。1、下载基础镜像根据docker-compose-base.yml及docker-compose.yml中的i...

看,教你手写一个最简单的SpringBoot Starter

何为Starter?想必大家都使用过SpringBoot,在SpringBoot项目中,使用最多的无非就是各种各样的Starter了。那何为Starter呢?你可以理解为一个可拔插式...

《群星stellaris》军事基地跳出怎么办?解决方法一览

《群星stellaris》军事基地跳出情况有些小伙伴出现过这种情况,究竟该怎么解决呢?玩家“gmjdadk”分享的自己的解决方法,看看能不能解决。我用英文原版、德语、法语和俄语四个版本对比了一下,结果...

数据开发工具dbt手拉手教程-03.定义数据源模型

本章节介绍在dbt项目中,如何定义数据源模型。定义并引入数据源通过Extract和Load方式加载到仓库中的数据,可以使用dbt中的sources组件进行定义和描述。通过在dbt中将这些数据集(表)声...

docker compose 常用命令手册_docker-compose init

以下是DockerCompose常用命令手册,按生命周期管理、服务运维、构建配置、扩缩容、调试工具分类,附带参数解析、示例和关键说明,覆盖多容器编排核心场景:一、生命周期管理(核心命令...

RagFlow与DeepSeek R1本地知识库搭建详细步骤及代码实现

一、环境准备硬件要求独立显卡(建议NVIDIAGPU,8GB显存以上)内存16GB以上,推荐32GB(处理大规模文档时更高效)SSD硬盘(加速文档解析与检索)软件安装bash#必装组件Docker...

Docker Compose 配置更新指南_docker-compose配置

高效管理容器配置变更的最佳实践方法重启范围保留数据卷适用场景docker-composeup-d变更的服务常规配置更新--force-recreate指定/所有服务强制重建down→up流程...