Android ViewModel实现和原理

news2024/12/28 20:41:34

ViewModel实现和原理

  • 前言
  • 1. 使用
    • 1.1 gradle准备
    • 1.2 模拟场景
    • 1.3. LiveData和ViewModel
    • 1.4 更新数据
  • 2. 原理与源码解读
    • 2.1 添加观察者
    • 2.2 setValue
    • 2.3 post
  • 参考资料

前言

ViewModel的主要基于观察者的设计模式,他主要分为两个部分:

  1. 提供者Provider:在我们这里就是数据的提供者,当实体的数据改变的时候,会自动通知所有观察者,观察者收到通知后就可以做对应的数据。
  2. 观察者Observer:观察者注册提供程序,当提供者每次发送通知的时候,观察者就会做对应的处理。

其最终实现出来的效果就是,在代码中,一旦我们注册的实体对象里面的数据改变之后,对应的UI就会自动的进行更新,这样数据更新的代码我们只需要在观察者里面写一套,不需要反复写多套了。

提供者和观察者是一对多的关系,也就是一个提供者可以被很多个观察者注册

1. 使用

1.1 gradle准备

在用上ViewModel之前,需要在项目的build.gradle中加上如下内容,开启DataBinding。

为了写ui方便我把ViewBinding也加上了,他不是必须的,但是我的demo代码里面会有ViewBinding相关内容。

android {
	buildFeatures {
		dataBinding = true
        viewBinding = true
    }
}

1.2 模拟场景

我们模拟一个简单的场景,页面里面就三个TextView,两个按钮,我们的目标就是用ViewModel来完成点了按钮之后他的Text就动态修改的功能。

实体是我随便设置的

class Student(var name: String = "",
              var age: Int = 0,
              var id: String = "")

页面长这样,三个TextView和两个按钮
在这里插入图片描述

1.3. LiveData和ViewModel

任何一个ViewModel的动态更新都需要围绕LiveData和ViewModel这两个类进行

LiveData:将实体动态化,可以被具有生命周期的对象观察到,只有这个目标对象的生命周期处于活动中,才会收到通知。
ViewModel:通信类,LiveData通过ViewModel来下发通知。

所以我们的代码最后写成这样:
ViewModel作为容器类,里面的成员变量是一个Student的LiveData类(一般都用MutableLiveData,如果有特殊需求也可以自己写)。
如果实际开发的时候有多个这样需要通信的实体,都丢到自定义的ViewModel类里面

class StudentViewModel : ViewModel() {
    private var student = MutableLiveData<Student>()

    fun getStudent(): MutableLiveData<Student> {
        return student
    }
}

注册通知的方式也很简单,先通过ViewModelProvider生成一个ViewModel的实体,然后将对应的实体进行观察者的注册即可。

lass ViewModelActivity : AppCompatActivity() {

    private lateinit var binding: ActivityViewModelBinding
    private lateinit var viewModel: StudentViewModel

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityViewModelBinding.inflate(layoutInflater)
        setContentView(binding.root)

		// 创建ViewModel实体的固定写法
        viewModel = ViewModelProvider(this).get(StudentViewModel::class.java)
        // 注册Student对象,这样Student这个对象一旦改变,就会自动调用这里面的方法
        viewModel.getStudent().observe(this) {
            binding.vmName.text = "姓名:${it.name}"
            binding.vmAge.text = "年龄:${it.age}"
            binding.vmId.text = "id:${it.id}"
        }
    }
}

1.4 更新数据

更新数据有两种方式:
setValue:整个对象改变之后,他会自动的通知更新。
post:只改变目标对象里面的一两个成员变量时,通过Post进行更新。

class ViewModelActivity : AppCompatActivity() {

    private lateinit var binding: ActivityViewModelBinding
    private lateinit var viewModel: StudentViewModel

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
    	//上面有的代码就不贴了

        binding.vmBtn1.setOnClickListener {
            val student = Student(name = "张三", age = 16, id = "001")
            viewModel.setStudent(student)
        }

        binding.vmBtn2.setOnClickListener {
            viewModel.setName("李四")
        }
    }
}
class StudentViewModel : ViewModel() {
    private var student = MutableLiveData<Student>()

    //通过调用setValue进行更新,代码写成这样是因为kotlin语法省略
    //注意,setValue这个方法必须要在主线程进行
    fun setStudent(student: Student) {
        this.student.value = student
    }

	// 只更新局部成员变量,通过postValue进行更新
    fun setName(name: String) {
        this.student.value?.name = name
        student.postValue(student.value)
    }

    fun getStudent(): MutableLiveData<Student> {
        return student
    }
}

2. 原理与源码解读

这里先把ViewModel在代码层面上执行的原理先讲了,然后我们再通过源码看一下他具体是怎么实现的:

  1. 添加观察者就是在LiveData里面弄了一个HashMap,key是观察者的实体,value是观察者和生命周期对象的绑定类。
    等于是说我们实际上创建的这个观察者,即监听LiveData这个可以变动的实体,也监听了页面本身的生命周期。
  2. setValue就是当key更新了之后,就遍历这个HashMap的keySet,将生命周期状态为运行中的key,调用他们的回调。
  3. post就是通过handler来进行下方通知,所以post可以在子线程跑,其他的部分和setValue一样。

从他的这个原理我们也可以看到观察者模式这种设计模式的一般代码思路:

  1. 添加观察者就是找个集合,List,Set,HashMap等,把观察者对象装进去,观察者对象在注册的时候一般都会传入一个回调CallBack。
  2. 下发通知就是触发某个通知方法之后,遍历集合,然后挨个调用他们注册时传入的回调。

2.1 添加观察者

为了方便我们看源码,我先把当时我们调用observe的这行代码还原成Java的样子

	viewModel.getStudent().observe((LifecycleOwner)this, (Observer)(new Observer() {
         public void onChanged(Object var1) {
            this.onChanged((Student)var1);
         }

         public final void onChanged(Student it) {
            TextView var10000 = ViewModelActivity.access$getBinding$p(ViewModelActivity.this).vmName;
            Intrinsics.checkNotNullExpressionValue(var10000, "binding.vmName");
            var10000.setText((CharSequence)("姓名:" + it.getName()));
            var10000 = ViewModelActivity.access$getBinding$p(ViewModelActivity.this).vmAge;
            Intrinsics.checkNotNullExpressionValue(var10000, "binding.vmAge");
            var10000.setText((CharSequence)("年龄:" + it.getAge()));
            var10000 = ViewModelActivity.access$getBinding$p(ViewModelActivity.this).vmId;
            Intrinsics.checkNotNullExpressionValue(var10000, "binding.vmId");
            var10000.setText((CharSequence)("id:" + it.getId()));
         }
      }));

我们实际上是new了一个Observer对象,然后将这个对象作为入参传了进来。这个东西看着其实和我们的onClickListener之类的很像,其实就是个回调的监听。

public abstract class LiveData<T> {

	// 其实就是一个map,我们不用去关心他里面的具体代码原理,知道他是一个map,有和hashmap同款的功能即可
	private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
            new SafeIterableMap<>();

	public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
		// 主线程判断,不是主线程就抛出异常
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // 生命周期已经死亡的就无视
            return;
        }
		// 创建了另一个观察者类,这个观察者类是负责观察页面生命周期的
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        // key为生命周期实体,value为观察者和页面生命周期的观察者类
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
        	// 如果这个监听已经被注册过了,就会抛出异常
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        // 将observer本身对页面的生命周期做监听
        owner.getLifecycle().addObserver(wrapper);
    }
}

mObservers就是我们所说的那个HashMap,key是观察者,也就是我们的new的那个observer类,value则是一个LifecycleBoundObserver对象,他是负责监听页面的生命周期的。

我们就通过这种方式即监听了LiveData本身,又监听了页面的生命周期。

2.2 setValue

接下来我们看看他是怎么做到动态更新的。从setValue这个方法开始,为了方便看,这里省略一些和原理无关的逻辑代码。

public abstract class LiveData<T> {
	@MainThread
    protected void setValue(T value) {
    	// setValue方法也必须在主线程执行
        assertMainThread("setValue");
        dispatchingValue(null);
    }

	void dispatchingValue(@Nullable ObserverWrapper initiator) {
        do {
            if (initiator != null) {
              //省略代码,我们入参是null 
            } else {
                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    // 重点看这行,这里就是遍历所有的监听执行
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        
    }

	private void considerNotify(ObserverWrapper observer) {
		// 页面生命周期判断,不符合就return了
		if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        // 数据的版本号判断,版本号不符合就return了
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        // 调用onChanged,onChanged就是我们的回调方法
        observer.mObserver.onChanged((T) mData);
	}
}

这样一看逻辑就很清晰了,每次我们调用setValue,他就会跑一个for循环,把所有的observer都做一个检测,符合要求的就调用最后的onChanged,不符合的就不调用。

2.3 post

我们最后在过一下post这条线的更新逻辑,这边本质上逻辑也是一样的

public abstract class LiveData<T> {

	volatile Object mPendingData = NOT_SET;
	
	private final Runnable mPostValueRunnable = new Runnable() {
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            // 本质上还是调用setValue,回到2.2了
            setValue((T) newValue);
        }
    };

	protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            mPendingData = value;
        }
        if (!postTask) {
            return;
        }
        // 无视掉同步锁的那些代码,本质就是跑了这一行,这个方法从名字上就知道是把一个东西post到主线程执行
        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }
}

代码一眼看到头,通过一个线程池将一个runnable推到主线程去处理,最后还是调用的setValue。

最后看一眼他这个线程池是怎么推到主线程的。

public class ArchTaskExecutor extends TaskExecutor {

	// TaskExecutor是个抽象类,实现是DefaultTaskExecutor
	private TaskExecutor mDelegate;

	private ArchTaskExecutor() {
        mDefaultTaskExecutor = new DefaultTaskExecutor();
        mDelegate = mDefaultTaskExecutor; 
    }
	
	@Override
    public void postToMainThread(@NonNull Runnable runnable) {
        mDelegate.postToMainThread(runnable);
    }
}

public class DefaultTaskExecutor extends TaskExecutor {
	
	private volatile Handler mMainHandler;
	
	public void postToMainThread(@NonNull Runnable runnable) {
        if (mMainHandler == null) {
            synchronized (mLock) {
                if (mMainHandler == null) {
                    mMainHandler = createAsync(Looper.getMainLooper());
                }
            }
        }
        //noinspection ConstantConditions
        mMainHandler.post(runnable);
    }
}

参考资料

https://developer.android.google.cn/topic/libraries/architecture/viewmodel/viewmodel-factories?hl=zh-cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1831446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Codesys 获取系统年、月、日、时、分、秒、星期几 +解决时区问题+ ST语言编程实现代码

一、 效果如图所示 二、功能说明 发现获取的时间比北京时间多一个时区&#xff08;8个小时&#xff09;&#xff0c;解决时区问题获取时间后&#xff0c;单独把年月日时分秒提取出来&#xff0c;单独保存在变量中获取星期几&#xff0c;保存在变量中 三、Codesys用ST语言实现…

【MySQL统计函数count详解】

MySQL统计函数count详解 1. count()概述2. count(1)和count(*)和count(列名)的区别3. count(*)的实现方式 1. count()概述 count() 是一个聚合函数&#xff0c;返回指定匹配条件的行数。开发中常用来统计表中数据&#xff0c;全部数据&#xff0c;不为null数据&#xff0c;或…

【C++】模板初级

【C】模板初级 泛型编程函数模板函数模板的概念函数模板格式函数模板的原理函数模板的实例化模板参数的匹配原则 类模板类模板格式类模板的实例化 泛型编程 当我们之前了解过函数重载后可以知道&#xff0c;一个程序可以出现同名函数&#xff0c;但参数类型不同。 //整型 voi…

如何获得一个Oracle 23ai数据库(vagrant box)

准确的说&#xff0c;是Oracle 23ai Free Developer版&#xff0c;因为企业版目前只在云上&#xff08;OCI和Azure&#xff09;和ECC上提供。 前面我博客介绍了3种方法&#xff1a; Virtual ApplianceRPM安装Docker 今天介绍最近新出的一种方法&#xff0c;也是我最为推荐的…

如何使用任意浏览器远程访问本地搭建的Jellyfin影音平台

文章目录 前言1. Jellyfin服务网站搭建1.1 Jellyfin下载和安装1.2 Jellyfin网页测试 2.本地网页发布2.1 cpolar的安装和注册2.2 Cpolar云端设置2.3 Cpolar本地设置 3.公网访问测试4. 结语 前言 本文主要分享如何使用Windows电脑本地部署Jellyfin影音服务并结合cpolar内网穿透工…

Linux---系统的初步学习【项目一:Linux操作系统的安装与配置】

项目一 Linux操作系统的安装与配置 1.1 项目知识准备 1.1.1 操作系统是什么&#xff1f; ​ 操作系统&#xff08;Operating System&#xff0c;OS&#xff09;是管理计算机硬件与软件资源的计算机程序。操作系统需要处理如管理硬件、决定程序运行的优先次序、管理文件系统等…

遗传算法浅理解

1. 什么是遗传算法&#xff1f; ​ 遗传算法&#xff0c;又称为 Genetic algorithm(GA)Genetic algorithm(GA)。其主要思想就是模拟生物的遗传与变异。它的用途非常广泛&#xff0c;可以用于加速某些求最大或者最小值的算法&#xff08;换句话说就是加速算法收敛&#xff0c;最…

月球全月地质图和4.5亿像素月面标注地图

嫦娥六号都在月球挖到土特产了&#xff0c;那你知道月球到底长什么样子吗&#xff1f; 现在我们就为你分享一下月球的全月地质图&#xff0c;以及4.5亿像素月面带标注的地图&#xff0c;你可以在文末查看该数据的领取方法。 月球全月地质图 对于月球的探索&#xff0c;美国和…

如何高效使用大型语言模型 LLMs 初学者版本 简单易上手

第一条也是最重要的一条规则是 永远不要要求LLM提供你无法自己验证的信息, 或让它完成你无法验证其正确性的任务。 唯一例外的情况是那些无关紧要的任务&#xff0c; 例如&#xff0c;让大型语言模型提供公寓装修灵感之类的是可以的 。 首先请看两个范例 不佳示范&#xff1a…

SAP 采购订单 价格 条件权限控制 授权账户

采购订单 价格 条件权限控制 授权账户 1、事务代码 me21/22/23/N 2、权限对像如下几个 M_BEST_BSA/EKG/EKO/WRK ACTVT 09 SELECT DISTINCT a.* FROM ( SELECT DISTINCT agr_users.uname FROM agr_1251INNER JOIN agr_users ON agr_1251.agr_name agr_users.agr_name AND…

智能制造uwb高精度定位系统模块,飞睿智能3厘米定位测距芯片,无人机高速传输

在科技日新月异的今天&#xff0c;定位技术已经渗透到我们生活的方方面面。从手机导航到自动驾驶&#xff0c;再到无人机定位&#xff0c;都离不开精准的定位系统。然而&#xff0c;随着应用场景的不断拓展&#xff0c;传统的定位技术如GPS、WiFi定位等&#xff0c;因其定位精度…

WPF 深入理解一、基础知识介绍

基础知识 本系列文章是对个人 B站 up 微软系列技术教程 记录 视频地址 https://www.bilibili.com/video/BV1HC4y1b76v/?spm_id_from333.999.0.0&vd_source0748f94a553c71a2b0125078697617e3 winform 与 wpf 异同 1.winform 项目结构 编辑主要是在 Form1.cs(页面)&#…

Go基础编程 - 09 - 通道(channel)

通道&#xff08;channel&#xff09; 1. 声明2. channel的操作3. 无缓冲通道4. 有缓冲通道5. 如何优雅的从通道循环取值6. 单向通道7. 异常总结 上一篇&#xff1a;结构体 Go语言的并发模式&#xff1a;不要通过共享内存来通信&#xff0c;而应该通过通信来共享内存。 Go语言…

Spring框架永远滴神之SpringAI玩转大模型

文章目录 一、SpringAI简介1.什么是SpringAI2.SpringAI支持的大模型类型&#xff08;1&#xff09;聊天模型&#xff08;2&#xff09;文本到图像模型&#xff08;3&#xff09;转录&#xff08;音频到文本&#xff09;模型&#xff08;4&#xff09;嵌入模型&#xff08;5&…

报错:C1189#error: The <experimental/filesystem> header providing 解决方案

今天开发过程中&#xff0c;需要使用文件系统experimental/filesystem&#xff0c;报错C1189#error: The &#xff1c;experimental/filesystem&#xff1e; header providing &#xff0c;通过以下解决方案&#xff0c;成功运行程序。 目录 一、打开项目下的属性 二、选择C/…

离散数学-代数系统证明题归类

什么是独异点&#xff1f; 运算 在B上封闭&#xff0c;运算 可结合&#xff0c;且存在幺元。 学会合理套用题目公式结合律 零元&#xff1f; 群中不可能有零元 几个结论要熟记&#xff1a; 1.当群的阶为1时&#xff0c;它的唯一元素视作幺元e 2.若群的阶大于1时&#xff0c;…

PV180R1K1T1NMMC派克通轴传动结构柱塞泵

PV180R1K1T1NMMC派克通轴传动结构柱塞泵 派克柱塞泵的结构组成部分&#xff1a;柱塞、手把、斜盘、压盘、滑履、泵体、配油盘、传送轴。其优点如下&#xff1a; 1、结构紧凑耐用&#xff0c;具有灵活的安装接口 2、安静的工作 3、效率高 4、降低功耗和减少发热 5、具有“…

视角概述( Perspective 业务分析篇)

背景 在业务分析工作中使用透视图来提供对特定于计划上下文的任务和技术的关注。大多数提案可能涉及一个或多个视角。视角主要包括&#xff1a; •敏捷•商业智能•信息技术•商业架构&#xff0c;以及业务流程管理。这些视角并不代表业务分析实践的所有可能视角。 任何给定…

SpringMvc—域对象共享数据和视图

一、向request域创建对象 先创建首页&#xff1a; 在testController这个类中&#xff1a; package com.pon.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; Controller public class test…

【UE5.1】制作自己的载具

目录 前言 效果 步骤 一、制作载具模型 二、载具设置 三、控制载具 前言 在前面我们通过UE4完成了载具的制作&#xff0c;下面我们介绍一下如何通过UE5制作载具。 效果 步骤 一、制作载具模型 制作方法同【UE4 制作自己的载具】1-使用3dsmax制作载具 二、载具设置 …