重新学习Android——(四)ViewModel
内容概览#
本篇介绍ViewModel
的概念,解决的问题,它的用法,以及真正的使用它来开发一个功能。
如果是跟着本系列笔记学习,无需担心该篇笔记中出现之前未出现过的内容,该笔记最后实现一个依赖于LifecycleObserver
和ViewModel
的小功能,完全不依赖LiveData
。
从问题出发#
注意,当下文中提及“组件”的概念时,如非另有说明,指的是
Activity
、Fragment
等Android SDK API中的组件
我们在进行Android开发时会面临一个问题,很多用户或系统的操作会让组件重建。
比如用户进行旋转屏幕时,Activity
会依次经过如下生命周期:
onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->
这相当于完全进行了一次重建,这会产生什么问题呢?
当我们使用传统方式进行开发时,我们所有的数据都在onCreate
(或其他生命周期方法)中进行初始化,那么当Activity
经历上面的重建流程后,所有之前的数据都会丢失。官方称这个问题为瞬态数据丢失
。我们可以使用onSaveInstanceState
方法和Bundle
来进行这些瞬态数据的保存、处理,但是这将非常麻烦,它只能存储简单的对象,对于复杂的对象,必须让这个对象可串行化。
其实问题的根源在于,我们的数据初始化完全和组件的生命周期绑定,想个办法让它独立于组件的生命周期要比想办法如何在组件重建时保存数据并在稍后恢复数据更加靠谱。
第二个问题就是,组件承担了太多代码使得组件的逻辑混乱,不可测试。根据应用架构指南中的说法,组件只应该处理UI事件和渲染页面,它不应该直接保存UI状态,UI状态应该由一个个的State Holders
来处理。
ViewModel解决这些麻烦#
ViewModel
具有比组件更长的生命周期,它不会因为组件Destroy
而结束,它需要等到它依赖的组件Finished
后才会真正结束,这时ViewModel
的onCleared
方法被调用。
ViewModel
的生命周期为第一次从组件中请求获得它开始到这个组件Finished
并销毁。
正是因为ViewModel
的生命周期长过组件,所以在ViewModel
中请不要存在任何依赖组件生命周期的代码,也不要存在任何视图、Activity上下文的引用
看到这里,第一个问题迎刃而解了,而第二个问题,似乎不用说,因为当我们打算创建ViewModel时,我们就是要把UI状态从组件中移动到ViewModel。
第一个小案例#
class UserViewModel : ViewModel() {
val users = arrayListOf(
User("于老八", 12, "女", "ASDFASDFASDFASDFASDF"),
User("于老九", 13, "女", "ASDFASDFASDFASDFASDF"),
User("于老十", 12, "女", "ASDFASDFASDFASDFASDF"),
User("于老十一", 12, "女", "ASDFASDFASDFASDFASDF"),
)
override fun onCleared() {
super.onCleared()
Log.i("UserViewModel", "ViewModel onCleared")
}
}
class MainActivity : AppCompatActivity() {
lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ...
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
textview.text = "共有${viewModel.users.size}名用户"
}
}
做个小实验,这时你旋转你手机的屏幕,并查看Logcat,你不会看到UserViewModel
中打印的那条ViewModel onCleared
的消息。而当你真正的结束MainActivity
时(比如结束进程),你就会看到这条消息。
第二个案例,Fragment通信#
我们想要创建这样的功能,主界面中有两个Fragment,左侧是所有用户的列表,右侧是当前选中的用户详情。使用之前的方法开发,这种情况不太容易处理。
我们需要在Activity
中写额外的代码来控制它们的交互逻辑,左侧的Fragment
当选中用户时要向Activity
上传一个事件,Activity
接到这个事件就要把左侧Fragment
选中的用户对象通过一些办法传递给右侧Fragment
,这一切交互都需要定义良好的接口来完成,每产生一次交互就要定义一个接口。
听起来就很难受,我们来看看使用ViewModel
咋写
先来完成左侧的Fragment
class UserListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val userListView = view.findViewById<RecyclerView>(R.id.user_list)
userListView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
// 通过requireActivity获取Activity的ViewModel
val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)
// 当某一个列表项被选中时,调用viewModel的select方法选中用户
userListView.adapter = UserListAdaper(requireContext(), viewModel.users) { user ->
viewModel.select(user)
}
}
// ...
}
在Fragment中,其实我们使用了和Activity中一样的办法来获取ViewModel,只不过我们使用requireActivity
以容纳Fragment的Activity的身份来获取。回想之前说的:ViewModel
的生命周期为第一次从组件中请求获得它开始到这个组件Finished
并销毁。如果这里是第一次通过这个Activity的身份获取该ViewModel,那么这个ViewModel的生命周期就开启了,无论你在哪里再用这个Activity的身份来获取这个ViewModel,得到的都是同一实例,直到这个Activity完成,ViewModel被销毁。
现在我们来写UserViewModel
中选中用户的逻辑
class UserViewModel : ViewModel() {
// ...
private var selectedUser: User? = null
fun select(user: User) {
selectedUser = user
}
}
这里使用了一个私有属性来记录当前选中的用户,代表当前选中的用户不希望直接被组件们访问,一会我们会使用其它的方法在右侧用户详情Fragment中得到它。
然后编写右侧的用户详情Fragment。
class UserDetailsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ...
val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)
viewModel.addOnUserSelectedListener {
username.text = it.name
ageAndSex.text = "${it.age}岁 ${it.sex}"
desc.text = it.description
}
}
}
我们看到,这里,我们采用了在ViewModel
中提供一个addOnUserSelectedListener
方法,当用户被选中时,ViewModel会回调这个函数,这个函数负责更新UI。
注意,这里的用词虽然是UserSelectedListener
,但实际上这也是个观察者模式。像LifecycleObserver
能够观察(observe)LifecycleOwner
生命周期的改变一样,这里,ViewModel
作为被观察者,Fragment
作为观察者,它观察用户选中状态的改变并做出更新UI的响应。等我们用到LiveData
时,这里的实现会更加简单,因为LiveData
本身就是可观察对象。
下面是addOnUserSelectedListener
的实现:
private val onUserSelectedListeners: MutableSet<(User) -> Unit> = mutableSetOf(
fun addOnUserSelectedListener(listener: (User) -> Unit) {
onUserSelectedListeners.add(listener)
// 当该listener添加之前已经有用户被选中了,先回调一下
selectedUser?.let {
listener(it)
}
}
上面的实现中,使用一个私有的回调函数集合来保存回调函数,并且如果添加该回调之前已经选择过用户,那么直接使用这个已有用户调用一次回调。目的是如果已有已选用户,那么就让添加回调的组件直接显示出来,否则还要等下一次选中用户才会显示。
现在我们就已经实现了功能了,可以运行下试试。
但是!!!还没完,还没完。想想如果右侧的Fragment因为什么原因结束了或者重建了会发生什么?它之前添加的回调还在ViewModel
的回调集合中,而这些回调已经没用了,即使调用,也什么都不会发生,因为添加它们的原来的Fragment已经没了。这就是内存泄漏。
我们应该在Fragment结束时将它添加的回调移除。
就是我们要写这样的代码:
class UserViewModel : ViewModel() {
// ...
fun removeOnUserSelectedListener(listener: (User) -> Unit) {
onUserSelectedListeners.remove(listener)
}
}
class UserDetailsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewModel.addOnUserSelectedListener(listener)
}
override fun onDestroy() {
super.onDestroy()
viewModel.removeOnUserSelectedListener(listener)
}
}
看看上面的代码,我们把操作userSelectedListener
的添加和移除的活放在了UserDetailsFragment
的生命周期函数中。在本例中显然无伤大雅,因为它太简单了,但这不正是上一篇笔记中学习的Lifecycle
库最适合干的活吗?正好操练下。
class UserSelectedListener(
private val viewModel: UserViewModel,
private val userSeletedListener: (User) -> Unit
) : DefaultLifecycleObserver{
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
viewModel.addOnUserSelectedListeners(userSeletedListener)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
viewModel.removeOnUserSelectedListeners(userSeletedListener)
}
}
创建一个UserSelectedListener
实现DefaultLifecycleObserver
,并将添加监听和移除监听的功能写在这里。
而在UserDetailsFragment
中我们就可以这样写:
val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)
lifecycle.addObserver(UserSelectedListener(viewModel) {
username.text = it.name
ageAndSex.text = "${it.age}岁 ${it.sex}"
desc.text = it.description
})
使用ViewModel
时,因为UI状态不再写在Activity中,而是写在ViewModel中,所以即使是页面中的Fragment也能轻易的获取Activity的ViewModel,两侧的Fragment都只需要与ViewModel交互即可,Activity被解放了。
并且,ViewModel只是定义UI状态,并给组件暴露获取这些状态的接口,而它并不关心是谁获取了这些状态。而且两个Fragment之间也并不知道对方的存在。
至此,加上上一篇笔记,我们已经把生命周期相关、UI状态相关的操作全部移到了组件外部,这给我们的程序带来更好的可阅读性和可测试性。
参考#
作者:Yudoge
出处:https://www.cnblogs.com/lilpig/p/15757343.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
欢迎按协议规定转载,方便的话,发个站内信给我嗷~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通