【Android Jetpack系列】五、ViewModel和LiveData的使用

2023-11-02

ViewModel和LiveData的使用

时(摸)隔(鱼)了小半个月, 终于开始了ViewModel和LiveData.

首先, 在开始之前, 说明一下: 什么是ViewModel? 什么又是LiveData?

什么是ViewModel?

老套路, ViewModel英文直译: 视图模型。官方原话:ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据。

的确, 这话很官方, 用直白的话来讲: ** 视图(View)数据(Data) 的桥梁(MVVM亦或者MVC中都有的概念), 是将视图数据分离开来, 降低UI与数据的耦合性, 即提高代码的可读性、可维护性。**

在官方的介绍中, ViewModel可谓是掌控全局, 就因为它活得久。

生命周期onCreate开始, 一直到onDestroy结束, 期间不管应用是切后台、还是转前台, 它都不会消失。

以下又是官方原话:

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。

ViewModel的生命周期描述图.png
还有需要特别注意的是:

ViewModel通常是在onCreate中被开发者手动构建, 而它会在onDestroy时被系统自动抹除onCleared()

!注意! ViewModel 绝对不要引用任何携带Context(包括Lifecycle也是携带有Activity)的对象; 正如上面所说, ViewModel命长, 对Context的引用可能会造成内存泄露, 如果确要使用Context可以试试AndroidViewModel, 因为AndroidViewModel默认携带了一个Application

什么是LiveData?

LiveData的英文直译: 具有生命的数据, 它是可观察的数据存储类(这里又出现了: 观察者模式), 那既然是具有生命的我们前文提到的Lifecycle就派上用场了。

那么LiveData中的可观察就是生命周期(Lifecycle)的可观察了。

好了, 这里需要补充一下前文Lifecycle的内容。

lifecycle.png

前面这张图上Log了一个currentState表示当前的生命周期状态, 不难看出currentState和生命周期函数的命名是一样的。

LiveData只会通知currentState处于STARTEDRESUMED状态下的数据进行更新, 在其它情况下LiveData会判定为非活跃状态, 将不会对这些状态下的数据下发更新通知。

该扯的都扯完了, 接下来进入正文。

本文所用开发环境以及SDK版本如下,读者应该使用不低于本文所使用的开发环境.

Android Studio 4.0.1
minSdkVersion 21
targetSdkVersion 30

正文

ViewModel的基本使用

在使用LiveData之前我们需要先自定义自己的ViewModel视图模型.

class MainVM : ViewModel() {}

然后在MainActivity.kt中构建它, 这里介绍两种方式。

方式一, 通过 ViewModelProvider 构建:

    private lateinit var mainVm: MainVM
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        mainVm = ViewModelProvider(MainActivity@ this).get(MainVM::class.java)
        //mainVm = ViewModelProvider(MainActivity@ this, ViewModelProvider.NewInstanceFactory()).get(MainVM::class.java)
    }

方式二, 通过kotlin扩展依赖构建(仅适用kotlin):

  • 首先在App级的build.gradle中, 添加kotlin扩展依赖
    androidx所支持的依赖.

      def fragment_version = "1.3.1" /// 本文所依赖的开发环境不支持1.3.1以上的版本, 读者自行更改版本号
      implementation "androidx.activity:activity-ktx:$fragment_version"
      implementation "androidx.fragment:fragment-ktx:$fragment_version"
    

    构建版本号列表androidx.activity
    构建版本号列表androidx.fragment

  • 然后修改MainActivity.kt中的代码

    import androidx.activity.viewModels
    class MainActivity : AppCompatActivity() {
        private lateinit var binding: ActivityMainBinding
        private val mainVm: MainVM by viewModels() /// 委托加载
        override fun onCreate(savedInstanceState: Bundle?) {
          binding = ActivityMainBinding.inflate(layoutInflater)
          setContentView(binding.root)
        }
    }
    

到这一步, ViewModel就已经能够被使用了, 它将一直存活至当前Activity结束。

LiveData的基本使用

LiveData自己实现了一套观察者机制, 它在监测到数据改变之后会自动的到通知observe()方法, 你可以在observe()方法中书写UI更新的代码。

计数器这个例子好像已经被写烂了, 但是不妨碍它是最简单的例子。

count.gif

LiveData需要配合ViewModel一起使用, 使用很简单, 一行代码就能搞定。

我们修改代码MainVM.kt中的代码

  class MainVM : ViewModel() {
    var count: MutableLiveData<Int> = MutableLiveData()
  }

然后, 就可以在MainActivity.kt中调用它。

  import androidx.activity.viewModels
  class MainActivity : AppCompatActivity() {
      private lateinit var binding: ActivityMainBinding
      private val mainVm: MainVM by viewModels()
      override fun onCreate(savedInstanceState: Bundle?) {
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        /// 只需要在这里写观察回调, liveData将自动监测数据变化
        mainVm.count.observe(MainActivity@ this) {
            binding.myTextView.text = "$it"  //setText()
        }

        /// add按钮, 设置点击事件, 直接更改 count 的值
        binding.add.setOnClickListener {
            mainVm.count.value = (mainVm.count.value ?: 0) + 1
        }
      }
  }

好了, 这个例子就这么完了, ViewModel和LiveData的使用到此结束。

LiveData和ViewModel在Fragment中的使用

LiveData和ViewModel的强大不止于此, 在此之前Framgent之间的通信很繁琐, 然而有了ViewModel之后, 彻底被简化了。

上图, 上代码!

首先是Fragment容器

fragment.png

activity_second.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/firstFragment"
        android:name="com.example.vl.fragment.FirstFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#55fafa00" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginVertical="16dp"
        android:background="@color/colorPrimary" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/secondFragment"
        android:name="com.example.vl.fragment.SecondFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#558787dd" />
</LinearLayout>

然后是fragment_first.xml

firstFragment.png

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.FirstFragment">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="FirstFragment"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/minus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="minus"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>

再然后是fragment_second.xml

secondFragment.png

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.SecondFragment">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SecondFragment"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/increase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="increase"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>

布局画好了之后, 就是代码了, Fragment只列出关键代码了。

FirstFragment.kt

class FirstFragment : Fragment() {
    private val secondVM:SecondVM by activityViewModels()
    ...
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentFirstBinding.inflate(inflater, container, false)

        secondVM.number.observe(requireActivity()) {
            binding.textView.text = "$it"
        }

        binding.minus.setOnClickListener {
            secondVM.onMinus()  //递减
        }

        return binding.root
    }
}

SecondFragment.kt

class SecondFragment : Fragment() {
    private lateinit var secondVM: SecondVM
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        secondVM = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()).get(SecondVM::class.java)
    }
    ...
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        // Inflate the layout for this fragment
        binding = FragmentSecondBinding.inflate(inflater, container, false)

        secondVM.number.observe(requireActivity()) {
            binding.textView2.text = "$it"
        }

        binding.increase.setOnClickListener {
            secondVM.onIncrease()  //递增
        }

        return binding.root
    }
}

SecondVM.kt

class SecondVM : ViewModel() {
    private var _number: MutableLiveData<Int> = MutableLiveData()
    val number: LiveData<Int> = _number //LiveData不允许通过 setValue 和 postsValue 更新数据

    fun onIncrease() {
        //前文没提, 这里的 value = ... 因为kotlin的特性, 调用的是 setValue() 方法
        _number.value = (_number.value ?: 0) + 1
    }

    fun onMinus() {
        _number.value = (_number.value ?: 0) - 1
    }
}

SecondFragment.kt

class SecondActivity : AppCompatActivity() {
    //private val secondVM: SecondVM by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
    }
}

这里可以不用书写 secondVM: SecondVM by viewModels() 为什么?

因为我们创建ViewModel对象并不是通过new去创建的(也千万不要通过new去创建), 而ViewModelProvider.Factory下文将做解释。

ViewModelProvider会寻找内存中已经被创建的ViewModel, 如果没有找到, 那么它会去创建一个新的。

我们跑一遍上面的代码。

running.gif

可以发现, 不管是点击上面的按钮, 还是点击下面的按钮, 两个TextView的内容都发生了改变。

LiveData和MutableLiveData

通过上面的代码发现, 我们既使用LiveData又使用了MutableLiveData, 那么它们的关系如何呢? 为什么说LiveData不能修改(更新)数据?

我们看到MutableLiveData的源码

MutableLiveDataSource.png

发现它是直接继承LiveData的, 虽然重写了setValue()postValue()这两个方法, 但是没有加入任何新的代码, 而是直接super父类方法。

LiveDataSource.png

点开父类LiveData才发现, 原来MutableLiveData只做了一件事情, 就是将protected -> public 了。

至于postValuesetValue的区别:在于postsValue处理多线程模式下的数据更新(但是UI的更新还是在主线程下), setValue只能在单线程模式下使用(你可以试试在多线程中调用setValue的情况)

值得注意的是postsValue可能会造成数据丢失, 具体查看【面试官:你了解 LiveData 的 postValue 吗?】这篇文章, 这里就不做赘述了。

为什么要用ViewModelProvider去构建ViewModel?

前面提到了千万不要通过new去创建ViewModel, 至于原因, 我们这里简单看一下ViewModelProvider的相关源码。

ViewModelProvider_get.png

上图可以看到, ViewModelProvider内部维护了一个private final ViewModelStore mViewModelStore, 如果没有找到对应的ViewModel, 那么就通过mFactory.create(modelClass)去创建实例(ViewModelFactory马上登场)。

view_model_store.png

ViewModelStore内部就是通过HashMap的特性来确保ViewModel的唯一性。

上面提到了, get()方法通过ViewModelProvider.ViewModelFactorycreate(modelClass)方法来创建实例。

而且, 我们在使用ViewModelProvider的时候, 通常都提供了一个ViewModelProvider.NewInstanceFactory(), 来看到下图它的结构。

create.png

反射!好家伙, 原来ViewModel实例就在这里创建的。

自定义ViewModelFactory

既然知道了ViewModel是通过反射创建的, 那就好办了; 现在有个需求: 在ViewModel初始化的时候, 需要参数初始化

图来!!

threevm.png

Transformations详解【点介里哇】

就需要我们自己实现ViewModelProvider.ViewModelFactory

/// 自定义 ViewModelFactory
class ThreeViewModelFactory(var user: User) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(User::class.java).newInstance(user)
    }
}

自定义 ViewModelFactory只需要继承ViewModelFactory` 然后重写create方法即可。

通过getConstructor(User::class.java)获取到带参数构造函数, 然后newInstance传入user实例。

最后在activity中使用即可

class ThreeActivity : AppCompatActivity() {
    private lateinit var binding: ActivityThreeBinding
    private lateinit var vm: ThreeVM
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityThreeBinding.inflate(layoutInflater)
        setContentView(binding.root)

        vm = ViewModelProvider(this, ThreeViewModelFactory(User("张三", 18))).get(ThreeVM::class.java)

        // 观察数据变化
        vm.userName.observe(this) {
            binding.nameText.text = it
        }

        // 点击按钮改变user
        binding.changeName.setOnClickListener {
            // kotlin中的三目运算
            vm.setName(if (vm.userName.value == "李四") "张三" else "李四")
        }
    }
}

运行结果图

running.gif

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Android Jetpack系列】五、ViewModel和LiveData的使用 的相关文章

  • 三大特性之继承

    继承 作用 还原客观世界中事物与事物的一种 is a 关系 1 is a 关系 即什么是一种什么 如图所示 比如 鸟 是一种 动物 鸟 is a 动物 香蕉 是一种 水果 香蕉 is a 水果 机械键盘 是一种 键盘 是一种 工具 机械键盘
  • c语言实现一个单元测试框架(Unit Test Framework)

    csdn lidp 转载注明出处 此单元测试框架为我在google code上的开源项目spider tool的一部分 关于spider tool 欢迎访问google code https spider tool googlecode c
  • 数据结构(一):顺序表

    使用typedef为现有类型创建别名 定义易于记忆的类型名 typedef 还可以掩饰复合类型 如指针和数组 void malloc unsigned int size 其作用是在内存的动态存储区中分配一个长度为 size 的连续空间 此函
  • /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start‘

    对于这个错误 不用想太多 一定是你再使用Vscode时没有事先保存这个 cpp c源文件就用 g xxx cpp o xxx gcc xxx c o xxx 来生成可执行文件导致的 hell cpp include

随机推荐

  • 最小二乘法和偏导

    偏导 在数学中 一个多变量的函数的偏导数 就是它关于其中一个变量的导数而保持其他变量恒定 相对于全导数 在其中所有变量都允许变化 求对 x 的偏导数 视 y 为常量 对 x 求导 求对 y 的偏导数 视 x 为常量 对 y 求导 最小二乘法
  • vSphere Client无法连接到服务器 出现未知错误的解决方法

    VMware ESXi服务器虚拟机在正常使用过程中 有时候会突然出现远程连接不上的问题 那么这个时候使用vSphere Client连接会出现如下错误 虽然连接不上 但是可以ping通 所以分析有可能是虚拟机用于客户端连接的服务停止了 可以
  • Linux内核调试方法总结之strace ,ltrace, ptrace, ftrace, sysrq

    come from https www cnblogs com justin y lin tag E5 86 85 E6 A0 B8 strace 用途 strace是一个功能强大的调试 分析 诊断工具 跟踪程序或进程执行时的系统调用和所接
  • OpenGL入门教程

    OpenGL入门教程 参考 OpenGL入门教程 Opengl 图形学final project作业记录 文章目录 OpenGL入门教程 一 概述 1 OpenGL 2 OpenGL ES与WebGL 3 OpenGL发展史 4 OpenG
  • iOS实现七牛多图片、文件上传和下载

    最近做项目用到了七牛的图片云存储服务 坑爹的是七牛只支持单图片的上传 但是谁会只传一张图片 要想实现多图片的上传必须自己实现多图片上传 网上相关资源又比较少 而且很多人都遇到了类似的问题 这里我总结了网上的一些零散的方法 自己写了个多文件上
  • 笔记本外置显卡说明

    一 简介 1 说明 笔记本外置显卡是一种硬件的配置方式 忽略笔记本自带的显卡 使用外置显卡 一方面可以自由选择显卡的类型 另一方面 可以实现高分辨率的正常显示 2 用途 在高清屏幕上展示画面 兼容高分辨率的游戏 下面详细说明一下这一配置的实
  • 【AI面试】目标检测中one-stage、two-stage算法的内容和优缺点对比汇总

    在深度学习领域中 图像分类 目标检测和目标分割是三个相对来说较为基础的任务了 再加上图像生成 GAN VAE 扩散模型 keypoints关键点检测等等 基本上涵盖了图像领域大部分场景了 尤其是在目标检测 一直是各大比赛 Pascal VO
  • 小目标检测论文阅读

    下面记录了一些论文的阅读总结 算法发展历程 传统图像算法 传统图像算法使用hand made feature 常用方法有SIFT HOG 图像金字塔等 对于小目标的检测 传统图像算法有人工复杂度高 模型泛化性差等缺点 因此逐渐被深度学习模型
  • t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL

    we will explore more sophisticated s f st ke t d 复杂的 trading strategies employed by leading market participants in the a
  • 巨潮网怎么下载年报_企业年报可别忘记,操作流程在这里。

    本专栏目录在文尾 企业年报 取代了之前的营业执照年检操作 所以是每个企业上半年中必须完成的一项重要工作 而此项工作有的企业会划归到财务部负责 这里就介绍一下企业年报的操作流程 在进行年报之前 有几个重要事项需要先了解一下 企业年报的申报期为
  • Esp32中Wi-Fi 开发介绍及使用:AP模式与STA模式常用函数详解

    目录 一 介绍 二 AP模式下常用函数 1 将 Wi Fi 作为接入点启动 2 使用函数 softAP 配置 Wi Fi AP 特性 3 使用函数softAPConfig配置静态IP 网关 子网掩码 4 使用函数softAPdisconne
  • Proxy代理模式

    list list Proxy代理模式是一种结构型设计模式 主要解决的问题是 在直接访问对象时带来的问题 比如说 要访问的对象在远程的机器上 在面向对象系统中 有些对象由于某些原因 比如对象创建开销很大 或者某些操作需要安全控制 或者需要进
  • python ssl连接 证书验证失败_Python SSL证书验证错误

    I m using requests to access a RESTful API Everything seems to work I can authenticate pull back a session token and eve
  • C语言,结构体中字符串的声明(采用字符指针还是字符数组)

    结构体中 字符串选项是用字符数组表示好 还是用字符指针表示好 typedef struct person char name int age char sex 6 该结构体中name用的是指针而不是数组 所以需要给字符串在堆上申请内存然后再
  • 服务失效判断

    题目描述 某系统中有众多服务 每个服务用字符串 只包含字母和数字 长度 lt 10 唯一标识 服务间可能有依赖关系 如A依赖B 则当B故障时导致A也故障 依赖具有传递性 如A依赖B B依赖C 当C故障时导致B故障 也导致A故障 给出所有依赖
  • liuseroj 网址

    点我进入liuseroj 如果打不开请点击备用线路
  • Java如何将文件下的所有文件进行批量更改和替换

    Java如何将文件下的所有文件进行批量更改和替换 将F盘下tmp文件夹下的文件循环取出 进行文件的替换 这里将李替换成1 修改完成写入到文件夹中 public class Test public static void main Strin
  • 高德地图内存泄露LocationManager$GnssStatusListenerTransport.mGnssCallback

    在使用高德地图的时候 喜提了一个内存泄露 GC Root Global variable in native code android location LocationManager GnssStatusListenerTransport
  • Altium Designer 详细入门教程-原理图绘画-AD2016

    这是我两年使用AD2016的总结和归纳 介绍AD2016原理图绘画的基本操作和比较高级的操作 主要面向0基础入门学习的爱好者 操作平台 win10 Altium Dsigner 2016 建议电脑使用屏幕较大的 因为有些对话框不太友好 或者
  • 【Android Jetpack系列】五、ViewModel和LiveData的使用

    ViewModel和LiveData的使用 时 摸 隔 鱼 了小半个月 终于开始了ViewModel和LiveData 首先 在开始之前 说明一下 什么是ViewModel 什么又是LiveData 什么是ViewModel 老套路 Vie