ViewModel, 不止MVVM

这是我参与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实列,因此抽取公共的功能接口TabAnimObservableTabAnimConsumer作为动画的派发者和消费者,同时使用一个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来获取所属ActivityViewModel,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可以作为ViewModelScope,我们可以从View中拿到Fragmentscope 么?

通过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.

猜你喜欢

转载自juejin.im/post/7034686185729425439