前言
在上一篇中,对Jetpack有了初步的认知,也详细讲解了LifeCycle的实际使用。在本篇中,将会对Jetpack对应的ViewModel进行详解!
1、ViewModel
ViewModel诞生之前出现的问题:
- 瞬态数据丢失
- 异步调用的内存泄露
- 类膨胀提高维护难度和测试难度
ViewModel的作用
如图所示
- 它是介于View(视图)和Model(数据模型)之间的桥梁
- 使视图和数据能够分离,也能保持通信
ViewModel的生命周期特性
如图所示
我们看到:
- ViewModel的生命周期长与Activity的生命周期,因此在使用ViewModel时,不要向ViewModel传入Activity的Context,因为这样会导致内存泄露!
- 如果硬要使用Context,那就使用AndroidViewModel中的Application
概念说了,接下来实战:
1.1 实战一(未使用ViewModel)
布局文件就一个TextView,所以这里就不贴布局代码了!
class Step1Activity : AppCompatActivity() {
private var textView: TextView? = null
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textView)
}
fun plusNumber(view: View) {
count++
textView!!.text = "$count"
}
}
这里我们看到,当我们点击按钮时,全局变量count 自加一,随后给textView 赋值!
来看看运行效果(屏幕旋转前)
保持这个值,旋转屏幕,看看效果:
当屏幕旋转时,我们发现用这种方式,自加的数据丢失了!
当然这个肯定有解决方案,这里也并不是解决这个方案出现的问题的。只是为了和ViewModel作个对比!
那现在看看使用ViewModel将会是怎样的?
1.2 实战二(使用ViewModel)
MyViewModel.kt
class MyViewModel(application: Application) : AndroidViewModel(application) {
var number = 0
}
这里代码很简单,就继承了AndroidViewModel ,里面定义了number 变量。
来看看使用
class Step2Activity : AppCompatActivity() {
private var textView: TextView? = null
private var viewModel: MyViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textView)
viewModel =
ViewModelProvider(this, AndroidViewModelFactory(application))[MyViewModel::class.java]
textView!!.text = String.valueOf(viewModel!!.number)
}
fun plusNumber(view: View) {
textView!!.text = String.valueOf(++viewModel!!.number)
}
}
这里我们看到,实例化对应的viewModel 后,通过访问viewModel 里的属性实现的逻辑。
这时我们运行发现,不管怎么切换横竖屏,对应的屏幕上的数字都不会被清空,也就是说,并没有出现之前的数据丢失的情况! 在这就不放运行效果图了哈。
接下来下一个组件!
2、LiveData
LiveData和ViewModel的关系
如图所示
它们对应关系是:在ViewModel中数据发生变化时通知页面
既然如此,那就开始实战校验一番!
2.1 实战一(简单应用)
上面说了,LiveDta需要有ViewModel才会发挥其作用,因此:
MyViewModel.kt
class MyViewModel : ViewModel() {
private var linkNumber: MutableLiveData<Int>? = null
fun getLinkNumber(): MutableLiveData<Int>? {
if (linkNumber == null) {
linkNumber = MutableLiveData()
linkNumber!!.value = 0
}
return linkNumber
}
fun addLinkedNumber(n: Int) {
linkNumber!!.value = linkNumber!!.value!! + n
}
}
我们可以看到,在这对应的ViewModel 里,定义了MutableLiveData<Int> 对应的LiveData集合,然后就是一顿初始化,修改等方法。
来看看怎么使用的?
class MainActivity : AppCompatActivity() {
private var viewModel: MyViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
viewModel =
ViewModelProvider(this, AndroidViewModelFactory(application))[MyViewModel::class.java]
viewModel!!.getLinkNumber()!!.observe(this, Observer {
textView.text = String.valueOf(it)
})
}
fun reduce(view: View) {
viewModel!!.addLinkedNumber(-1)
}
fun add(view: View) {
viewModel!!.addLinkedNumber(1)
}
}
这个页面的布局很简单,就两个按钮和一个文本,因此就没有贴对应的布局代码。
这里我们看到:
- 初始化ViewModel和上面如出一辙,当点击对应按钮时,将通过
addLinkedNumber 对ViewModel对应LiveData属性做了修改操作! - 随后activity通过
viewModel!!.getLinkNumber()!!.observe(this, Observer {xx} ,来接受ViewModel发过来的修改通知,并及时更新至textView上
来看看运行效果:
哈哈哈,有点简陋哈!不过只要效果达到了就行!
接下来进入下一个实战!
2.2 实战二(Fragment通信)
2.2.1 对应的布局文件
fragment.xml (两个相同的布局)
<?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=".FirstFragment">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:min="0"
android:max="100"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity布局 如图所示
首页布局,就两个Fragment,上下各一个,平均分配,每个Fragment布局相同,都含有相同的SeekBar
想要实现的效果:不管拖动哪个Fragment里面的SeekBar,另一个SeekBar跟着移动
2.2.2 对应实现逻辑
先看MyViewModel.kt
class MyViewModel : ViewModel() {
private var progress: MutableLiveData<Int>? = null
fun getProgress(): MutableLiveData<Int>? {
if (progress == null) {
progress = MutableLiveData()
progress!!.value = 0
}
return progress
}
}
狠简单,直接看怎么使用!
FirstFragment.kt
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var rootView = inflater.inflate(R.layout.fragment_first, container, false)
var seekBar = rootView.findViewById<SeekBar>(R.id.seekBar)
val viewModel = ViewModelProvider(
requireActivity(), AndroidViewModelFactory(
requireActivity().application
)
)[MyViewModel::class.java]
viewModel.getProgress()!!.observe(requireActivity(), Observer {
seekBar.progress = it
})
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
viewModel.getProgress()!!.setValue(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
return rootView
}
}
我们看到,这里使用方式如出一辙,当seekBar 属性值发生改变时,将修改LiveData的值,随后通知对应Fragment达到刷新UI的作用!
第二个Fragment实现逻辑和这个一模一样,直接复制粘贴重命名一份。
来看看运行效果
完美,当然使用了ViewModel,任凭旋转屏幕哈,数据都不会丢失!
2.3 总结
从上面的实战中可以看出LiveData的优势
- 确保界面符合数据状态
- 不会发生内存泄露
- 不会因Activity停止而导致崩溃
- 不再需要手动处理生命周期
- 数据始终保持最新状态
- 适当的配置更改
- 共享资源
结束语
好了,本篇到这里差不多结束了,相信读者对ViewModel+LiveData有所了解了!在下一篇中将会详解DataBinding以及将前面所讲解的集合成一个:LifeCycle+ViewModel+LiveData+DataBinding实战!
Demo下载:点我下载(包含下一篇部分源码)
|