欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ViewModel 的基本用法

程序员文章站 2022-06-08 14:13:28
...

ViewModel简介

ViewModel 应该算是Jetpack 中最重要的组件之一了。其实Android 平台上之所以会出现注入MVP、MVVM 之类的项目架构,就是因为在传统的开发模式下,Activity 的任务实在是太重了,既要负责逻辑处理,又要控制UI 提示,甚至还得处理网络回调,等等。在一个小项目中这样写或许没有什么问题,但是如果在大型项目中仍然使用这样写法的话,那么这个项目将会变得非常臃肿并且难以维护,因为没有任何架构上的划分。

ViewModel 的一个重要作用就是可以帮助Activity 分担一部分工作,它是专门用于存放于界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity 中,这样可以在一定程度上减少Activity 中的逻辑。

另外,ViewModel 还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候,Activity 会被重新创建,同时存放在Activity 中的数据也会丢失。而ViewModel的生命周期和Activity 不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity 退出的时候才会跟着Activity 一起销毁。因此,将与界面相关的变量存放在ViewModel 当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。ViewModel 的生命周期如图所示:
ViewModel 的基本用法

ViewModel 的基本用法

由于Jetpack 中的组件通常是以 AndroidX 库的形式发布的,因此一些通常的Jetpack 组件会在创建AndroidX 项目时自动被包含进去。不过如果我们想要使用 ViewModel 组件,还需要在app/build.gradle 文件中添加如下依赖:

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

通常来讲,比较好的编程规范是给每一个 Activity 和 Fragment 都创建一个对应的ViewModel ,因此这里我们就位 MainActivity 创建一个对应的 MainViewModel 类,并让它继承自ViewModel ,代码如下所示:

import androidx.lifecycle.ViewModel

class MainViewModel: ViewModel() {
}

根据前面所学的知识,所有与界面相关的数据都应该放在ViewModel 中。那么这里我们要实现一个计数器的功能,就可以在 ViewModel 中加入一个 counter 变量用于计数,如下所示:

class MainViewModel : ViewModel() {
    var counter: Int = 0
}

现在我们需要在界面上添加一个按钮,每点击一次按钮就让计数器加1,并且把最新的计数显示在界面上。修改activity_main.xml 中的代码,如下所示:

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

    <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" />

</LinearLayout>

布局文件非常简单,一个TextView 用于显示当前的计数,一个Button 用于对计数器加1。

接着我们开始实现计数器的逻辑,修改MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        plusOneBtn.setOnClickListener{
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter(){
        infoText.text = viewModel.counter.toString()
    }
}

先看下运行结果
ViewModel 的基本用法

注意:我们绝对不可以直接去创建ViewModel的实例,而是一定要通过ViewModelProvider 来获取 ViewModel 的实例,具体语法规则如下:

ViewModelProvider(<你的Activity 或 Fragment 实例>).get(<你的ViewModel>::class.java)

之所以这么写,是因为ViewModel 有其独立的生命周期,并且其生命周期要长于Activity 。如果我们在onCreate() 方法中创建一个ViewModel 的实例,那么每次onCreate() 方法执行的时候,ViewModel 都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了

除此之外的其他代码应该都是非常好理解的,我们提供了一个refreshCounter() 方法用来显示当前的计数,然后每次点击按钮的时候对计数器加1,并调用refreshCounter() 方法刷新计数

如果你尝试通过侧边工具栏旋转一下模拟器的屏幕,就会发现Activity 虽然被重新创建了,但是计数器的数据却没有消失

向ViewModel传递参数

上一小节中创建的 MainViewModel 的构造函数中没有任何参数,但是思考一下,如果我们确实需要通过构造函数来传递一些参数,应该怎么办呢?由于所有ViewModel 的实例都是通过ViewModelProvider 来获取的,因此我们没有任何地方可以向ViewModel 的构造函数中传递参数

当然,这个问题也不难解决,只需要借助ViewModelProvider.Factory 就可以实现了

现在的计数器虽然在屏幕旋转的时候不会丢失数据,但是如果退出程序之后再重新打开,那么之前的计数就会被清零了。接下来我们就对这一功能进行升级,保证即使在退出程序后又重新打开的情况下,数据仍然不会丢失。

相信你已经猜到了,实现这个功能需要在退出程序的时候对当前的计数进行保存,然后在重新打开程序的时候读取之前保存的计数,传递给MainViewModel 。因此,这里修改 MainViewModel 中的代码,如下所示:

class MainViewModel(countReserved: Int) : ViewModel() {
    var counter: Int = countReserved
}

现在我们给 MainViewModel 的构造函数添加了一个 countReserved 参数,这个参数用于记录之前保存的计数值,并在初始化的时候赋值给 counter 变量

前面已经说了需要借助 ViewModelProvider.Factory ,因此新建一个 MainViewModelFactory 类,并让它实现 ViewModelProviders.Factory 接口,代码如下所示:

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(countReserved) as T
    }
}

可以看到,MainViewModelFactory 的构造函数中也接收了一个 countReserved 参数。另外 ViewModelProvider.Factory 接口要求我们必须实现create() 方法,因此这里在create() 方法中我们创建了 MainViewModel 实例,并将 countReserved 参数传了进去。为什么这里就可以创建 MainViewModel 的实例了呢?因为create() 方法的执行时机和 Activity 的生命周期无关,所以不会产生之前提到的问题

另外,我们还得在界面上添加一个清零按钮,方便用户手动将计数器清零。修改activity_main.xml 中的代码,(在原来基础上增加一个按钮即可)如下所示:

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

修改 MainActivity 中的代码

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel
    private lateinit var sp:SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)

        viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
        plusOneBtn.setOnClickListener{
            viewModel.counter++
            refreshCounter()
        }
        clearBtn.setOnClickListener {
            viewModel.counter = 0
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter(){
        infoText.text = viewModel.counter.toString()
    }

    override fun onPause() {
        super.onPause()
        sp.edit{
            putInt("count_reserved",viewModel.counter)
        }
    }
}

现在重新运行程序,点击数次”Plus One“ 按钮,然后退出程序并重新运行,你会发现,计数器的值是不会丢失的,只有点击”Clear“ 按钮,计数器的值才会被清零。如图所示:
ViewModel 的基本用法

本章内容源自 郭霖大神的《第一行代码 第三版》