0x1、引言
来来来,继续学穿Jetpack,本节带来组件 → ViewModel
视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态,都一个意思,怎么称呼看你自己喜欢~
ViewModel 一言以蔽之
ViewModel 将 视图数据
从 视图控制器
中分离,并实现了 数据管理
的:一致性
、数据共享(跨页面通信)
及 作用域可控
。
① 视图数据与控制器分离
视图控制器
一般代指Activity和Fragment,它们通过在屏幕上绘制View,捕获用户事件,处理用户与互动界面相关的操作来 控制界面。
视图数据
就是你用来对控件setXxx()的数据源,它和与它相关的决策逻辑 (或者说管理) 不应该放到视图控制器中。
ViewModel所做的事,就是用 模版方法模式 进行封装,隐藏一些具体细节,提供简洁的API供我们使用。给了我们一种它们好像真的分离了的错觉,实际上还是与视图控制器紧密相连,ViewModel依旧被对应的Activity、Fragment所持有。
最直观的体现::页面配置变更,引起页面销毁重建,ViewModel中的数据不会因此而丢失
。
写个简单的例子更直观,先不用ViewModel:
class TestActivity : AppCompatActivity() { private var mCount: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_test) tv_content.text = "${mCount}" } bt_test.setOnClickListener { tv_content.text = "${++mCount}" } }
操作:点击按钮mCount会自增1,旋转手机触发屏幕翻转,发现数字又从0开始了:
接着用上ViewModel,定义一个类继承ViewModel,把mCount丢到里面:
class TestViewModel: ViewModel() { var mCount = 0 }
改动下原代码:
class TestActivity : AppCompatActivity() { private lateinit var testViewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_test) testViewModel = ViewModelProvider(this).get(TestViewModel::class.java) tv_content.text = "${testViewModel.mCount}" } bt_test.setOnClickListener { tv_content.text = "${++testViewModel.mCount}" } }
执行同样的操作,无论怎么翻转,数字都不会因页面重建而丢失(表现为从0开始)。
结论:把视图数据放到ViewModel里,就不会受页面配置变化销毁重建的影响。
② 一致性
对了,上面说到的配置变更,除了横竖屏切换外,还有这些:
分辨率调整、权限变更、系统字体样式变更、系统语言切换、多窗口设定、系统导航方式变更等。
在以前,为了避免这种 页面配置变更引起的页面销毁重建
导致的 视图数据丢失
问题,需要我们在 onSaveInstanceState()
和 onRestoreInstanceState()
手动编写数据保存和恢复的语句。
页面一多、要保存恢复的数据一多、加之多人协作,就很容易出现 结果不一致的问题,比如:某人在编写存数据相关的代码,漏掉了某个数据,导致拿时没拿到正确的数据。
结论:使用ViewModel,你只管把数据丢里面就行,无需关心具体如何存取,间接保证了结果一致性。
扩展一下:有了ViewModel就不需要onSaveInstanceState()了?
答:非也非也,具体用哪个还得权衡数据复杂度、访问速度及生命周期,建议 混合使用,分而治之。
怎么说?除了这两种存储恢复数据的方式外,还有一种 持久化存储,官方文档 《保存界面状态》 提供了一个维度参考表:
简要说下笔者的看法:
ViewModel
- 数据存内存中,读取更快,和Activity(或其他生命周期所有者)关联,配置更改期间保留在内存中,系统会自动将ViewModel与发生配置更改后产生的新Activity实例关联;
- 关联组件 (Activity或Fragment) 退出时,系统会自动销毁ViewModel,进程终止也会销毁;
- 适用于:配置更改后数据需要继续存在的场景,支持复杂对象。
onSaveInstanceState()
- 用Bundle存储数据以便于跨进程传递,存储上限受限于Binder(1M),请勿用于存储大量数据(如Bitmap),也不要存需要冗长序列化和反序列化操作的复杂数据结构;
- onSaveInstanceState()会将数据序列化到磁盘,如果序列化对象很复制,序列化时会占用大量内存,可能丢帧和视觉卡顿;
- 适用于:配置更改后少量数据、Activity异常关闭,进程被终止后重新打开 需要恢复的场景。
持久性存储
如果 数据的恢复非常重要、存储数据非常大、数据需要长期存储 的场景,可以考虑持久化存储,比如存数据库中。建议策略:间歇性提前自动把临时数据从内存中备份到硬盘中。当然,持久性存储不局限于本地,网络亦可。
③ 数据共享 (跨页面通信)
日常开发中,Activity和Fragment通信,Fragment与Fragment通信的场景非常常见,常见的做法下述几种:
- 依托于宿主Activity,定义一堆公共的访问Fragment的方法,setArguments() 或者 目标Fragment()预留回调;
- Fragment中调 getActivity() 获得宿主Activity,强转后获取FragmentManager实例,通过findFragmentById()或者ByTag()拿到目标Fragment实例,调用setArguments()传参;
- 利用Fragment Result的API,使用公共FragmentManager,充当传递数据的中心存储,setFragmentResult() 和 setFragmentResultListener();
- EventBus、广播等
各有利弊,而采用ViewModel,只需 指定作用域,即可轻松实现跨页面通信。写个烂大街的经典例子:点击列表Fragment,更新右侧内容Fragment,预期效果如下:
接着写代码实现一波,先是左侧列表项的布局 (item_list.xml):
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_choose" android:layout_width="match_parent" android:layout_height="60dp" android:background="@android:color/holo_green_light" android:gravity="center" />
接着到列表适配器类 (ListAdapter.kt)
class ListAdapter(data: ArrayList<String>): RecyclerView.Adapter<ListAdapter.ViewHolder>() { private var mData = data private var mClickListener: ItemClickListener? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent ,false)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.chooseTv?.let { it.text = mData[position] it.setOnClickListener { mClickListener?.onItemClick(mData[position]) } } } override fun getItemCount() = mData.size fun setOnItemClickListener(listener: ItemClickListener) { this.mClickListener = listener } inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { var chooseTv: TextView? = null init { chooseTv = itemView.findViewById(R.id.tv_choose) } } interface ItemClickListener { fun onItemClick(choose: String) } }