目录
简单介绍
ViewModel应该可以算是Jetpack中最重要的组件之一了。其实Android平台上之所以会出现诸如MVP、MVVM之类的项目架构,就是因为在传统的开发模式下,Activity的任务实在是太重了,既要负责逻辑处理,又要控制UI展示,甚至还得处理网络回调,等等。在一个小型项目中这样写或许没有什么问题,但是如果在大型项目中仍然使用这种写法的话,那么这个项目将会变得非常臃肿并且难以维护,因为没有任何架构上的划分。
而ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在 ViewModel中,而不是Activity中,这样可以在一定程度上减少Activity中的逻辑。
另外,ViewModel还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候, Activity会被重新创建,同时存放在Activity中的数据也会丢失。而ViewModel的生命周期和 Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。因此,将与界面相关的变量存放在ViewModel当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。ViewModel的生命周期如图所示。
基本使用
创建活动的ViewModel
先加入如下的依赖:
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
通常来说,比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel,因此这里我们就为MainActivity创建一个MainViewModel类,并且让他继承ViewModel,如下:
class MainViewModel : ViewModel {
// 可以添加成员变量和方法
}
所有与界面有关的数据都应该放在ViewModel中,那么我们要实现一个计数器的功能,就可以在ViewModel中加入一个counter变量用于计数,如下:
class MainViewModel : ViewModel() {
var counter = 0
}
现在需要在界面上添加一个按钮,每点击一次按钮就让计数器加一,并且把最新的计数器显示在界面上,改代码省略
现在开始实现计数器的逻辑,修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?{
super.onCreate(savedInstanceState)
setContentView(R.ayout .activity main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
plusOneBtn.setOnClickListener {
viewModel.counter++refreshCounter()
refreshCounter()
}
private fun refreshCounter() {
infoText.text = viewModel.counter.toString()
}
}
代码不长,我来解释一下。这里最需要注意的是,我们绝对不可以直接去创建ViewModel的实例,而是一定要通过ViewModelProvider来获取ViewModel的实例,具体语法规则如下:
ViewModelProvider(<你的Activity或Fragment实例>).get(<你的ViewModel>::class.java)
之所以要这么写,是因为ViewModel有其独立的生命周期,并且其生命周期要长于Activity。如果我们在onCreate()方法中创建ViewModel的实例,那么每次onCreate()方法执行的时候,ViewModel都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了。
除此之外的其他代码应该都是非常好理解的,我们提供了一个refreshCounter()方法用来显示当前的计数,然后每次点击按钮的时候对计数器加1,并调用refreshCounter()方法刷新计数。
两个碎片间的通信
但是,对于需要完成活动与碎片间通信或者碎片与碎片间通信时,创建时需要传入相同的context,接下来展示两个碎片间的通信:
class TopFragment : Fragment() {
private lateinit var mViewModel : MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fr_top,container,false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mViewModel = ViewModelProvider(requireActivity()).get(MainViewModel::class.java)
mViewModel.count.observe(viewLifecycleOwner){ count->
tv.text = count.toString()
}
}
}
class BottomFragment : Fragment() {
private lateinit var mViewModel : MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fr_bottom,container,false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mViewModel = ViewModelProvider(requireActivity()).get(MainViewModel::class.java)
btn.setOnClickListener {
mViewModel.count.value = mViewModel.count.value?.plus(1)
}
}
}
需要注意的是:
mViewModel = ViewModelProvider(requireActivity()).get(MainViewModel::class.java
ViewModelProvider传入的是activity而不是当前的fragment,道理很简单,我们要实现Fragemnt之间的数据共享或者通讯,必须要拿到相同的ViewModel实例,如果传入this,那就不是同一个ViewModel对象了。
以下代码是LifeDate的内容,之后我会更新,简单来说它的功能是当count值发生变化时,它会通知TopFragment。
mViewModel.count.observe(viewLifecycleOwner){ count->
tv.text = count.toString()
接下来是主活动:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
向ViewModel传递参数
由于所有ViewModel的实例都是通过ViewModelProvider来获取的,因此我们没有任何地方可以向VIewModel的构造函数中传递参数.
当然,这个问题也不难解决,只需要借助ViewModelProvider.Factory就可以实现了.
现在的计数器虽然在屏幕旋转的时候不会丢失数据,但是如果退出程序之后再重新打开,那么之前的计数就会被清零了.现在就要保证即使在退出程序后又重新打开的情况下,数据仍然不会丢失.
实现这个功能就需要在 退出程序的时候对当前的计数进行保存,然后在重新打开程序的时候读取之前保存的计数,并传递给MainViewModel,因此,这里修改MainViewModel中的代码,如下
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = countReserved
}
接下来的问题是如何向MainViewModel的构造函数传递数据了,就需要使用ViewModelProvider.Factory,下面看看如何实现.
新建一个MainViewModelFactory类,并让他实现ViewModelProvider.Factory接口
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
在布局中添加一个清零按钮,此处省略
然后修改MainActivity中的代码:
class MainActivity : AppCompatActivity(){
lateinit var viewModel: MainViewModel
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)
...
clearBtn.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
override fun onPause() {
super.onPause()
sp.edit {
putInt ("count reserved", viewModel.counter)
}
}
}
在onCreate()方法中,我们首先获取了SharedPreferences的实例,然后读取之前保存的计数值,如果没有读到的话,就使用0作为默认值。接下来在ViewModelProvider中,额外传入了一个MainViewModelFactory参数,这里将读取到的计数值传给了MainViewModelFactory的构造函数。注意,这一步是非常重要的,只有用这种写法才能将计数值最终传递给MainViewModel的构造函数。
剩下的代码就比较简单了,我们在“Clear”按钮的点击事件中对计数器进行清零,并且在
onPause()方法中对当前的计数进行保存,这样可以保证不管程序是退出还是进入后台,计数都不会丢失。
本文大部分内容皆来自郭霖的《Android第一行代码》第三版