这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战
前言
网上介绍ViewModel
的文章很多,基本都是介绍其作为数据的持有者,结合LiveData
,作为MVVM的一个成员,负责维护数据状态和派发来使用。
今天这篇文章不想介绍ViewModel
的原理,主要是想分享下我在开发中用ViewModel
又做了些啥。
解耦,数据生命周期及代码边界
组件化后造成的问题
App体积越来越大,经历了几次包体积优化,体积仍然120MB左右,为了加快编译速度,我们对App 做了模块解耦,在这里先不讨论模块解耦的好处,先讨论模块解耦的直观的影响,一是模块间粒度划分很难控制,分的粗了达不到预期效果,过细则易造成循环依赖;另一方面是模块间相互引用变得非常不便,甚至很简单的场景就需要加个Bridge,面条式代码非常影响美观,这何尝不是新的耦合呢。
场景1.数据初始化和销毁的时机
我们的App 首页是一个多Tab布局,有一份数据本来只有TAB A 使用,这次改版又增加了一个新的Tab B,Tab B 还是可有可无的,现在这份数据在 TAB A 和 B 都需要使用,怎么初始化这份数据呢? 由于知道多个Fragment
是可以共享一个ViewModel
的,因此把数据的初始化放在ViewModel
中就是一种可行的方案,生成一个合适的ViewModel
并监听需要的事件和合适的时机初始化就可以了。
class ConfigInitializer : ViewModel(), LifeCycleAware, LoginStateAware {
//...
init{
listenOnAction(action){
//doInit
}
}
}
复制代码
新增加了一个Tab,这个Tab随着用户退出登录会自动消失,我们怎么在用户退出登录时清空这Tab的数据呢?
首先清空数据的逻辑是不能放到这个Tab Fragemnt
中的,因为这个Fragemnt
是否显示是一个可选项,因此把数据销毁逻辑放在这个Tab里是会无法覆盖所有的场景的。作为模块化的架构,如果首页Fragment
对首页的生命周期感兴趣,可以注册一个简单的钩子方法(这里使用WMRouter
),这样子就可以不向主Module暴漏新的方法来完成数据清理逻辑,减少代码耦合。
@RouterService(interfaces = [BootPageLifeCycleObserver::class], singleton = true)
class MyModuleLifeCycleAware : BootPageLifeCycleObserver {
override fun onAppDestroy(app: Application) {
}
override fun onCreate(main: FragmentActivity) {
ViewModelProviders.of(main)[DisposeViewModel::class.java]
}
override fun onDestroy(main: FragmentActivity) {
}
}
复制代码
在DisposeViewModel
监听合适的事件,完成数据清理逻辑
class DisposeViewModel: ViewModel() {
init {
listenLogout {
MyModuleStore.clearCache()
}
}
}
复制代码
场景2. 跨Module的协调者
主页上每个Tab都是一块业务单元,分在一个单独的Module里,现在有个需求,Tab 上需要增加动画响应Tab对应Fragemnt
的布局变化,Tab 的单击和双击事件也会影响Fragment
布局的变化,如果没有做模块解耦,完成这个需求还是挺简单的。
现在面对的问题是Tab组件分布在主Module中,在页面对应的Module中是无法引用的,另一方面按照代码功能每个页面的动画逻辑应该局限在自己的Module里,不适宜抽取到公共的组件中,本着最小暴漏的原则,这里Tab 组件和每个对应的页面都能通过其context
拿到唯一的ViewModel
实列,因此抽取公共的功能接口TabAnimObservable
和TabAnimConsumer
作为动画的派发者和消费者,同时使用一个ViewModel
作为两者协调器完成生成者和消费者的连接。
这里使用ViewModel
有两个好处,一是两个Fragment
的最小公共生命周期是其承载的Activity
避免了使用全局单例,另一方面,向外只暴漏了功能接口,每个接口对应的具体功能完全没有暴漏到公共Module。
class TabAnimSupportCoordinator : ViewModel() {
private val consumerMap = mutableMapOf<String,TabAnimConsumer<Boolean>>()
private val driverMap = mutableMapOf<String, TabAnimObservable>()
fun addAnimTabDriver(id: String, driver: TabAnimObservable) {
driverMap[id] = driver
linkDriverAndConsumer(id)
}
fun addAnimTabConsumer(id: String, consumer: TabAnimConsumer<Boolean>) {
consumerMap[id] = consumer
linkDriverAndConsumer(id)
}
private fun linkDriverAndConsumer(id: String) {
val driver = driverMap[id]
val consumer = consumerMap[id]
if (driver != null && consumer != null) {
driver.observer(consumer)
}
}
fun onSingleTap(id: String) {
driverMap[id]?.onSingleTap()
}
fun onDoubleTap(id: String) {
driverMap[id]?.onDoubleTap()
}
fun onTabRebuild() {
//do clear on fragment recreate
}
}
复制代码
More Clean Code
View 中使用ViewModel
在日常开发中,如果有一些逻辑绑定在ViewModel
上,如何通过View来获取ViewModel
来避免参数层层传递呢?
通常我们可以使用View#getContext()
转型为FragmentActivity
来获取所属Activity
的ViewModel
,j减少代码耦合。
ViewModelProviders.of(view.context.unwrap())[FunctionViewModel::class.java]
fun Context.unwrap(): FragmentActivity {
var context: Context = this
while (context !is Activity && context is ContextWrapper) {
context = context.baseContext
}
return context as FragmentActivity
}
复制代码
如果场景比较复杂,比如在多Tab页面,我们的View
只出现在其中一个Fragment
页面中,继续使用FragmentActivity
来引用ViewModel
就不是非常合适了。
比如最近开发遇到的一个场景,竖向的RecycleView
中有多个横向的RecyclerView
,需求需要横向的RecyclerView
需要上次滑动停留的位置,功能实现并不难,需要我们仔细考虑的是怎么组织数据在合适的生命周期里,以及怎么避免影响已有的代码结构?
考虑到Fragment
可以作为ViewModel
的Scope
,我们可以从View
中拿到Fragment
的 scope
么?
通过View#``findViewTreeViewModelStoreOwner``()
我们可以简单的拿到控制View
生命周期的最直接的ViewModelStore
,拿到ViewModelStore
,在Fragment中对应的是FragmentViewLifecycleOwner
*,至此又可以方便的取到ViewModel
比如在Adapter
中,可以onBindViewHolder
恢复横向RecycleView
上次滚动的位置,无痛插入了新的业务逻辑。
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
init(viewHolder as VH)
recoverAndRecordOffset(viewHolder)
}
private fun recoverAndRecordOffset(viewHolder: VH) {
viewHolder.itemView.post {
val target = viewHolder.recycler
val owner = target.findViewTreeViewModelStoreOwner()
if (owner != null) {
val key = model::class.java.simpleName
OffSetChangeRecorderHolder.get(owner).recover(key, target)
OffSetChangeRecorderHolder.get(owner).record(key, target)
}
}
复制代码
考虑到ViewModel的生命周期比View长,上面的数据仍然可能在View销毁后保存在Fragment 中,考虑到数据量很小,问题不大,这个方式也不失为一个一个可行的解决方案了。
总结
合理使用ViewModel
,能让我们更好的规范数据的生命周期和处理代码的边界问题;当然更多的场景根本就不适合使用ViewModel
,没有ViewModel
,我们也需要合理的控制代码边界,仔细的规范数据的生命周期呀。
Happy Ending.