草庐IT

Jetpack学习之ViewModel、Lifecycles、LiveData

Gnocihz 2023-03-28 原文

Jetpack学习之ViewModel、Lifecycles、LiveData

声明:本学习笔记基于郭霖大大的《第一行代码 第3版》并结合官方文档、网络资源以及个人理解整理而成,欢迎大家讨论指正

Jetpack简介

主要组成

​ Jetpack是一个开发工具集,能够协助开发者编写出更简洁的代码,简化开发过程,并且这些组件有一个很好的特点,他们大部分不依赖与任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中,并且拥有非常好的向下兼容性。

​ Jetpack全家桶包含内容非常多,主要可分为 基础、架构、行为、界面 4个部分,本次学习主要聚焦于对架构的学习,其中很多组件更是专门为MVVM架构量身打造的。

使用前准备

​ Jetpack中组件通常都是以AndroidX库的形式发布的,所以常用的Jetpack组件会在创建Android项目时被自动包含进去,使用者个可以根据官方文档的说明,在app/build.gradle根据需要添加依赖

    dependencies {
        def lifecycle_version = "2.4.0-rc01"
        def arch_version = "2.1.0"

        // ViewModel
        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
        // LiveData
        implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
        // Lifecycles only (without ViewModel or LiveData)
        implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

        // Saved state module for ViewModel
        implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

        // Annotation processor
        kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
        // alternately - if using Java8, use the following instead of lifecycle-compiler
        implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

        // optional - helpers for implementing LifecycleOwner in a Service
        implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"

        // optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
        implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"

        // optional - ReactiveStreams support for LiveData
        implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"

        // optional - Test helpers for LiveData
        testImplementation "androidx.arch.core:core-testing:$arch_version"
    }
    

示例准备

​ 还有一个简单的,包含了一堆按钮的布局文件activity_main.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">

    <TextView
        android:id="@+id/infoText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="32sp"/>

    <Button
        android:id="@+id/plusOneBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Plus One"/>

    <Button
        android:id="@+id/clearBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Clear"/>

    <Button
        android:id="@+id/getUserBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Get User"/>

    <Button
        android:id="@+id/addDataBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Add Data"/>

    <Button
        android:id="@+id/updateDataBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Update Data"/>

    <Button
        android:id="@+id/deleteDataBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Delete Data"/>

    <Button
        android:id="@+id/queryDataBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Query Data"/>

    <Button
        android:id="@+id/doWorkBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Do Work"/>

</LinearLayout>

ViewModel

简介

​ Jetpack最重要的组件之一,可以帮任务太重的Activity分担一部分工作,用于专门存放界面相关数据,即界面上能看到的数据,其相关变量都应该存放在ViewModel中,而不是Activity,从而减轻Activity工作。

重要特性

​ 手机横竖屏旋转时,Activity会被重新创建,存放在Activity中的数据也会丢失,而ViewModel不会因为屏幕旋转而重新创建,只有当Activity退出时才会跟着Activity一起销毁,所以非常适合存放界面上的变量。

生命周期示意图

基本用法

​ 通常来讲,比较好的编程规范是要给每个ActivityFragment都创建一个对应的ViewModel,这里以创建一个简单的计数器为例

  1. MainActivity创建一个对应的MainViewModel类继承自ViewModel

    import androidx.lifecycle.ViewModel
    class MainViewModel: ViewModel() {
        //计数器变量
        var counter = 0
    }
    
  2. MainActivity中获取MainViewModel并调用

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.view.LayoutInflater
    import androidx.lifecycle.ViewModelProvider
    import com.example.jetpacktest.databinding.ActivityMainBinding
    
    class MainActivity : AppCompatActivity() {
    
        val binding: ActivityMainBinding by lazy{
            ActivityMainBinding.inflate(LayoutInflater.from(this))
        }
    	
        lateinit var viewModel: MainViewModel
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
            //注意:ViewModelProviders.of已弃用
            //注意:绝对不可以直接去创建ViewModel实例
            viewModel = ViewModelProvider(this)[MainViewModel::class.java]
    		//调用
            binding.plusOneBtn.setOnClickListener {
                viewModel.counter++
                refreshCounter()
            }
        }
        
    	//刷新函数
        private fun refreshCounter() {
            binding.infoText.text = viewModel.counter.toString()
        }
    
    }
    

    ​ 记住绝对不可以直接去创建ViewModel实例,一定要通过ViewModelProvider(ViewModelStoreOwner)构造函数来获取,具体语法规则为为viewModel = ViewModelProvider(<你的Activity或Fragment实例>)[<你的ViewModel>::class.java]

    ​ 之所以要这样写是因为每次旋转屏幕都会重新调用onCreate()方法,若每次都创建新的实例就无法保存数据了。用上述方法,onCreate方法被再次调用,它会返回一个与确切的与MainActivity相关联的预先存在的ViewModel,这就是保存数据的原因。

    悲报:

    ​ 在2019.08..07的版本更新中弃用了 ViewModelProviders.of()。您可以将 FragmentFragmentActivity 传递给新的ViewModelProvider(ViewModelStoreOwner) 构造函数,以实现相同的功能。

向ViewModel传递参数

​ 来看看需要通过构造函数传递参数的情况,主要借助ViewModelProvider.Factory实现,并且还能实现退出程序后重新打开仍然保留数据的功能

  1. 修改MainViewModel代码,添加构造函数

    class MainViewModel(countReserved: Int): ViewModel() {
        var counter = countReserved
    }
    
  2. 新建一个MainViewModelFactory类实现ViewModelProvider.Factory接口

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.ViewModelProvider
    
    class MainViewModelFactory(private val countReserved: Int): ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            //创建MainViewModel实例
            return MainViewModel(countReserved) as T
        }
    }
    

    ​ 构造器中接受了countReserved参数,并要求实现create()方法,并在里面创建MainViewModel实例,因为该方法的执行时机与Activity的生命周期无关,所以不会产生此前的问题。

  3. 修改MainActivity

    class MainActivity : AppCompatActivity() {
    	...
        lateinit var viewModel: MainViewModel
        lateinit var sp: SharedPreferences
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
            
            sp = getPreferences(Context.MODE_PRIVATE)
            val countReserved = sp.getInt("count_reservde",0)
    
            viewModel = ViewModelProvider(
                this, MainViewModelFactory(countReserved))[MainViewModel::class.java]
            ...
            binding.clearBtn.setOnClickListener { 
                viewModel.counter  = 0
                refreshCounter()
            }
            refreshCounter()
        }
    
        override fun onPause() {
            super.onPause()
            //对计数进行保存
            sp.edit {
                putInt("count_reservde",viewModel.counter)
            }
        }
        
    	...
    }
    

    ​ 在onCreate()方法中,我们先获取SharedPreferences的实例,用于关闭程序后保存数据。然后读取之前保存的计数值,如果没有读到就以0为默认值。要注意在ViewModelProvider()方法中多传入了一个参数,是将读取到的计数值传给了 MainViewModelFactory,这有这样才能将计数值传递给MainViewModel的构造函数。

Lifecycles

简介

Lifecycles用于感知Activity的生命周期(在非Activity类也可),下面是手写Lifecycle监听器的两步方法

基本用法

  1. 新建一个MyObserver类实现LifecycleObserver接口

    import android.util.Log
    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.LifecycleObserver
    import androidx.lifecycle.OnLifecycleEvent
    
    class MyObserver : LifecycleObserver{ //其实这是一个空方法接口,只需要实现声明即可
    	//利用注解功能,会在Activity的OnStart()触发时执行
        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        fun activityStart() {
            Log.d("MyObserver","activityStart")
        }
    
        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        fun activityStop() {
            Log.d("MyObserver","activityStop")
        }
        
    }
    

    ​ 利用注解功能传入相应得生命周期时间,需要注意的是除了熟悉的六种外还多了一种ON_ANY类型,可以表示以匹配Activity的任何生命周期回调

  2. 借助LifecycleOwner获取通知,在MainActivity中增加一行代码,MyObserver就能自动感应Activity的生命周期了

    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
            ...
        	//增加一行
            lifecycle.addObserver(MyObserver())
        	...
        }
    

    ​ 首先我们要了解LifecycleOwner的语法结构是:LifecycleOwner.lifecycle.addObserver(MyObserver()),而只要你的Activity是继承自AppCompatActivity,或者你的Fragment继承自androidx.fragment.app.Fragment的,那么它们本身就是一个LifecycleOwner实例,在AndroidX库中已经自动帮我们实现了,所以上示代码才可以这样写。

  3. 若是想要主动获取当前生命周期状态,只需在MyObserver的构造函数中传入Lifecycle对象,相应修改MainActivity即可

    class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver{ //其实这是一个空方法接口,只需要实现声明即可
    	...
    }
    
     lifecycle.addObserver(MyObserver(lifecycle))
    

生命周期对应状态示意图

LiveData

简介

LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

之前示例代码的缺陷:

​ 一直都是在Activity中操作,每次加一先给ViewModel 中的计数加一,然后再调用其获取,,而并不是ViewModel 主动通知返回给Activity,在多线程任务中容易或渠道错误的数据

解决方法:

Activity传给ViewModel ?那绝对不行,根据上示ViewModel 生命周期示意图,ViewModel 的生命周期是长于Activity的,若把Activity实例传给ViewModel很可能导致Activity无法释放从而造成内存泄露,这是一种非常错误的做法!正解当然就是用LiveData

基本用法

MutableLiveData是一种可变的LiveData,主要是3种读写数据的方法

  • getValue()用于获取LiveData中包含的数据

  • setValue()用于在主线程中给LiveData设置数据

  • postValue()用于在非主线程中给LiveData设置数据

    一般在Kotlin中会采用语法糖写法,如下例

  1. 修改上面写过的MainViewModel代码

    class MainViewModel(countReserved: Int): ViewModel() {
    
        var counter = MutableLiveData<Int>()
    
        init {//初始化之前保存的数据
            counter.value = countReserved
        }
    
        fun plusOne() {
            val count = counter.value ?:0
            counter.value = count + 1
        }
    
        fun clear() {
            counter.value = 0
        }
    
    }
    

    ​ 这里counter变量修改为一个MutableLiveData对象,泛型指定为Int表示整形数据。init结构体给counter设置数据,这样之前保存的数据可以在初始化时得以恢复。新增两个函数分别用于“加1”和“清零”操作,由于LiveDatagetvalue方法可能为空,所以这里用一个?:操作符,当获取到的数据为空时就用0为默认计数。

  2. 修改MainActivity

    override fun onCreate(savedInstanceState: Bundle?) {
    		...
            binding.plusOneBtn.setOnClickListener {
    //            viewModel.counter++
    //            refreshCounter()
                viewModel.plusOne()
            }
    
            binding.clearBtn.setOnClickListener {
    //            viewModel.counter = 0
    //            refreshCounter()
                viewModel.clear()
            }
    
            viewModel.counter.observe(this, Observer { count ->
                binding.infoText.text = count.toString()
            })
    
        }
    
        override fun onPause() {
            super.onPause()
            sp.edit {
                putInt("count_reservde", viewModel.counter.value ?: 0)
            }
        }
    

    ​ 在两个按钮对应的事件中都换成了对应的方法plusOne()clear(),在onPause()方法中也相应修改了写法,中间是最关键的修改地方:这里调用了viewModel.counterobserve()方法,需要传入两个参数,一是LifecycleOwner,在上文提到Activity本身也是LifecycleOwner,所以直接传入this,第二个参数是Observer接口,当counter中包含的数据发生变化时,就会回调到这里,我们也能借此更新到界面上。

    ​ 修改后的好处:不用担心ViewModel 开启线程执行耗时逻辑,同时也应注意此时是运行在主线程中,若要在子线程修改LiveData值则要注意一定要调用postValue()方法

  3. 以上就是LiveData的基础用法了,但还不算最规范的,主要问题在于我们把counter这个可变的LiveData暴露给了外部,这样即使是在ViewModel 外面也能为counter设置数据,从而破坏ViewModel 的封装性并存在一定风险。所以比较推荐的做法是,永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察LiveData的数据变化而不能设置LiveData数据

    class MainViewModel(countReserved: Int): ViewModel() {
    //    var counter = MutableLiveData<Int>()
    //    init { counter.value = countReserved }
    //    fun plusOne() {
    //        val count = counter.value ?:0
    //        counter.value = count + 1
    //    }
    //    fun clear() { counter.value = 0}
        
        val counter : LiveData<Int>
            get() = _counter
        
        private val _counter = MutableLiveData<Int>()
    
        init {
            _counter.value = countReserved
        }
    
        fun plusOne() {
            val count = _counter.value ?:0
            _counter.value = count + 1
        }
    
        fun clear() {
            _counter.value = 0
        }
    
    }
    

    ​ 相比原来,counter变量全部变为了_counter变量,并添加了private修饰符对外部不可见,然后重新定义一个counter变量声明为不可变的LiveData,并在它的get()属性方法中返回_counter变量,这样做当外部调用counter变量时,实际上获得的是_counter的实例,并无法给counter设置数据,从而保证了封装性,这也是官方的推荐写法,非常规范

map和switchMap

LiveData的两种转换方法:map()switchMap()

有关Jetpack学习之ViewModel、Lifecycles、LiveData的更多相关文章

  1. 嵌入式学习之QT学习----3 制作简单的QT界面(如:QQ登录界面) - 2

    1、创建一个QT工程newproject—>Application—>QtWidgetsApplication—>choose…(注意不要有中文路径)填写名称(我写的名称为class2)和创建路径(D:\qt\qt_demo\class2)—>填写类名,这里基类要选择“QWidget”,这样一个QT工程就创建好啦。qt的移植性非常强,一套代码我们不用修改太多,直接通用所有的平台。说明:QMainWindow:主窗口类,主窗口具有主菜单栏、工具栏和状态栏,类似于一般的应用程序的主窗口。QWidget:它是所有具有可视界面的基类,选择QWidget创建的界面对各种界面组件都可以支持。QDialog

  2. javascript - 如何在 Durandal 中为我的 shell viewmodel 使用一个类? - 2

    我正在查看HotTowel模板,并试图让它在TypeScript中工作,我在转换shellView模型时遇到了问题。我正在尝试将其转换为TS,这对我来说更有意义,它应该是一个类,而不是像所示那样简单地导出函数here.我看了thisapproach但是,注意评论here,决定不遵循它。经过一番挖掘,我找到了thisthread,这表明它应该像重写router.getActivatableInstance一样简单,但我似乎还无法调用该函数。这是我的main.ts(也包含在一个类中)://///////import_app=module('durandal/app');import_sys

  3. c# - 如何在 asp.net mvc 中使用 razor viewmodel 将 c# guid 值分配给 javascript 变量? - 2

    我有一个具有Guid类型属性的View模型。我需要将它分配给javascript对象属性并将该对象发布到某个操作方法。当我写的时候(在javascript中):varpartyId=@Model.Id;//"Id"isofGuidtype我明白了varpartyId=6abbf77d-ba28-4d8a-87ff-2fa8f8a070c9;//UncaughtSyntaxError:Unexpectedidentifier我该如何处理?我的意思是将Id值分配给javascript变量。 最佳答案 将@Model.Id括在引号内。

  4. javascript - 从 ViewModel 外部调用 Knockout 函数 - 2

    在我的具体示例中,有几个不同的小部件都有自己封装的ViewModel。我需要一个全局保存按钮来保存每个单独的ViewModel。我似乎无法弄清楚如何在各个ViewModel上调用函数。示例-如何在每个ViewModel上调用Save函数:http://jsfiddle.net/sNSh2/4/varViewModel1=function(){varself=this;self.firstName=ko.observable('');self.lastName=ko.observable('');self.firstName('John');self.lastName('Doe');se

  5. javascript - Firefox Jetpack 开发调试 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion因此,我在Firefox中使用一些JS,同时也在使用jetpacksdk。我在JS编辑器中工作,然后通过命令行运行sdk。但是,当存在错误时,无法调试它,因为JS在页面和DOM之前运行/加载,因此Firebug无济于事。谁能为此推荐一个

  6. javascript - knockout JS : Update/Insert data to a viewModel using mapping - 2

    我已经尝试解决这个问题很长一段时间了。我找不到任何解决此问题的方法,但如果我错了,请纠正我。问题:我有来自JSONAPI的数据,具有嵌套数组/对象结构。我使用映射最初用我的数据填充模型。要更新它,我想在新数据到达时扩展模型,或者更新现有数据。据我所知,映射选项键应该对我有用,但我可能误解了映射选项的功能。我已经归结了这个例子要表示的问题:varuserMapping={key:function(item){returnko.utils.unwrapObservable(item.id);}};//JSONcallreplacedwithvaluesvarviewModel={users

  7. javascript - 如何将现有的 DOM 元素绑定(bind)到 KnockoutJS viewModel - 2

    如果JS值发生更改,我将使用KnockoutJS更新DOM(Knockout为我们提供了此功能)。默认的KnockoutviewModel类似于以下block:Javascript:varviewModel={price:ko.observable(109)}HTML:现在,当价格发生变化时,Knockout会自动更新View。但我想要的是以下内容:varviewModel={price:ko.observable(jQuery("#price"))}99.00所以,我想将一个DOM元素绑定(bind)到我的viewModel。模型中的价格属性初始化为值99.00。当价格改变时(在Ja

  8. javascript - 尝试使用 Knockout ViewModel 实现 SignalR - 2

    我有以下代码,它似乎没有调用客户端并使用信息更新KnockOutJSView模型。GetOuting()在页面加载时被调用,并将正确的信息输入到View模型中。Chrome在开发者工具中没有显示JS错误。从那时起,我很难弄清楚如何解决它。任何帮助表示赞赏。SignalR中心publicclassOutings:Hub{privatestaticDictionaryoutings=newDictionary();publicvoidGetOuting(stringid){varguidID=newGuid(id);boolcontainsOuting=outings.ContainsKe

  9. javascript - Knockout JS 在 foreach 绑定(bind)中调用 ViewModel 函数 - 2

    让我们考虑一个使用knockout的View模型:vardata=[{id:1,name:"JohnDoe"},{id:2,name:""},{id:3,name:"PeterParker"}];varviewModel={items:ko.observableArray(data)};viewModel.showName=function(name){console.log(this);returnname&&name.length>0;};viewModel.removePerson=function(){console.log(this);};ko.applyBindings(v

  10. javascript - 如何将 ViewModel Store 绑定(bind)到 View? - 2

    我是ExtJS的新手,正在尝试嵌入MultiSelect在Panel里面.TheViewModelhasastorespropertyasyoucanseehere:Ext.define('TEST.view.controls.search.SearchFilterModel',{extend:'Ext.app.ViewModel',alias:'viewmodel.filter',data:{title:''},stores:{test:{fields:['id','name'],proxy:{type:'ajax',url:'api/test',reader:'array'},au

随机推荐