ViewModel 的基本用法

文章目录

  • ViewModel简介
  • ViewModel 的基本用法
  • 向ViewModel传递参数

ViewModel简介

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

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

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

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 中的代码,如下所示:


<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 的基本用法_第2张图片

注意:我们绝对不可以直接去创建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“ 按钮,计数器的值才会被清零。如图所示:

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

你可能感兴趣的:(Jetpack最全简析,viewmodel)