“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)(下)

简介: “无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)

生命周期安全 & 无内存泄漏


上面弹 toast 的 gif 图中有一个细节,触发搜索行为的瞬间并未弹出 toast,而是等到界面返回了历史页才弹出。


但代码明明是在触发搜索行为的时候就调用了的:


class SearchViewModel : ViewModel() {
    val rearrangeLiveData = MutableLiveData<String>()
    fun search(keyword: String) {
        ...
        // 在触发新搜索时提示
        rearrangeLiveData.value = "新搜索词汇排在最前面"
    }
}


因为观察数据是在历史页进行的。触发搜索联想的时候,历史页的生命周期已经走到了 onDestroyView(),即处于不活跃状态。


此时 LiveData 内部会对处于 destroy 状态的观察者进行清理。以保证数据不会再推送给不活跃的观察者,造成不必要的 crash。及时移除观察者也避免了更长生命周期的观察者持有界面造成内存泄漏的风险。(ViewModel 持有 LiveData,LiveData 持有观察者,观察者是匿名内部类,所以它持有界面引用)


关于这一点源码级别的分析可以点击LiveData 面试题库、解答、源码分析


所以使用 LiveData 时就特别省心,只管赋值就好,不用担心界面生命周期以及内存泄漏问题。


多界面共享业务逻辑


整个搜索业务中,触发搜索行为的有3个地方,分别是搜索页的搜索按钮(搜索 Activity)、点击搜索历史标签(历史 Fragment)、点击搜索联想词(联想 Fragment)。这三个触发点分别位于三个不同的界面。


在 MVP 架构中触发搜索的业务逻辑被封装在 SearchPresenter 的业务接口中:


class SearchPresenterImpl(private val searchView: SearchView) : SearchPresenter {
    // 历史列表
    private val historys = mutableListOf<String>() 
    override fun search(keyword: String, from: SearchFrom) {
        // 跳转到搜索结果页
        searchView.gotoSearchPage(keyword, from) 
        // 拉升搜索条
        searchView.stretchSearchBar(true) 
        // 隐藏搜索按钮
        searchView.showSearchButton(false) 
        // 更新历史
        if (historys.contains(keyword)) {
            historys.remove(keyword)
            historys.add(0, keyword)
        } else {
            historys.add(0, keyword)
            if (historys.size > 11) historys.removeLast()
        }
        // 刷新搜索历史
        searchView.showHistory(historys)
        // 搜索历史持久化
        scope.launch { searchRepository.putHistory(historys) }
    }
}


理论上,三个不同的界面应该都调用这个方法触发搜索,这使得搜索这个动作的业务实现内聚于一个方法内。但在 MVP 中要实现这一点不太容易。


最简单的办法是在 Fragment 中获取 Activity 实例,然后再获取其成员变量 SearchPresenter:


// SearchHintFragment.kt
private val presenter by lazy {
    (requireActivity() as? TemplateSearchActivity)?.searchPresenter
}


类型强转的代码都是耦合的,强转为 TemplateSearchActivity 就意味着和这个具体的 Activity 耦合,使得 SearchHintFragment 不能脱离它存在,也就没有单独复用的可能性(比如另一个搜索场景中联想页一模一样,它就无法被复用)


ViewModel 巧妙地解决了这个问题。


虽然 ViewModel 还是在 Activity 中构建,但它并不是直接存储在 Activity 中,而是存在了一个叫ViewModelStore的类中:


public class ViewModelStore {
    // 存放 ViewModel 的 HashMap
    private final HashMap<String, ViewModel> mMap = new HashMap<>();
    // 存 ViewModel
    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }
    // 取 ViewModel
    final ViewModel get(String key) {
        return mMap.get(key);
    }
}


Activity 负责提供 ViewModelStore:


// Activity 基类实现了 ViewModelStoreOwner 接口
public class ComponentActivity 
    extends androidx.core.app.ComponentActivity 
    implements LifecycleOwner, ViewModelStoreOwner{
        // Activity 持有 ViewModelStore 实例
        private ViewModelStore mViewModelStore;
        public ViewModelStore getViewModelStore() {
            if (mViewModelStore == null) {
                // 获取配置无关实例
                NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
                if (nc != null) {
                    // 从配置无关实例中恢复 ViewModel商店
                    mViewModelStore = nc.viewModelStore;
                }
                if (mViewModelStore == null) {
                    mViewModelStore = new ViewModelStore();
                }
            }
            return mViewModelStore;
        }
        // 静态的配置无关实例
        static final class NonConfigurationInstances {
            // 持有 ViewModel 商店实例
            ViewModelStore viewModelStore;
            ...
        }
}


其中 ViewModelStoreOwner 用于描述如何构建 ViewModelStore:


public interface ViewModelStoreOwner {
    ViewModelStore getViewModelStore();
}


Activity 中通过如下代码构建 ViewModel 实例:


// TemplateSearchActivity.kt
val searchViewModel: SearchViewModel = 
    ViewModelProvider(this)[SearchViewModel::class.java]


这行代码构建了 ViewModel 的实例,再把它存放在 TemplateSearchActivity 提供的 ViewModelStore 中。


子 Fragment 通过如下代码获取 父 Activity 的 ViewModel 实例:


val searchViewModel: SearchViewModel by activityViewModels<SearchViewModel>()


其中activityViewModels()是 androidx.fragment:fragment-ktx 提供的一个扩展方法:


public inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(
    VM::class, { requireActivity().viewModelStore },
    { requireActivity().defaultViewModelCreationExtras },
    // 默认使用 activity 提供的 ViewModelProvider.Factory
    factoryProducer ?: { requireActivity().defaultViewModelProviderFactory }
)
// 惰性构建 ViewModel
public fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    extrasProducer: () -> CreationExtras = { defaultViewModelCreationExtras },
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise, extrasProducer)
}
// 惰性的 ViewModel
public class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    // ViewModel 实例缓存
    private var cached: VM? = null
    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                // 构建 ViewModel 实例
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it // 缓存 ViewModel 实例
                }
            } else {
                viewModel
            }
        }
    override fun isInitialized(): Boolean = cached != null
}


activityViewModels() 将 ViewModel 的构建封装在一个Lazy里面,表示会惰性计算一次,计算完成后会存入缓存,下次直接获取。所以它必须配合关键词by一起使用。


构建 ViewModel 时传入的 ViewModelProvider.Factory 和 ViewModelStore 都是 activity 的,这样就可以在子 Fragment 中轻松的获取父 Activity 的声明的 ViewModel 了,实现 ViewModel 的共享。


共享 ViewModel 使得 Activity 和 Fragment 可以轻松地获取同一个 ViewModel 实例,所以在它们之间共享业务逻辑或传输数据都变得易如反掌。


新的复杂度


攻击窗口


那些介于同一变量多个引用点之间的代码称为 “攻击窗口”。可能会有新代码加到这种窗口中,不当地修改了这个变量,或者阅读代码的人可能会我忘记该变量应有的值。

一般而言,把对一个变量的引用局部化,即把引用点尽可能集中在一起总是一种很好的做法。这使得代码的阅读者,能每次只关注一部分代码。而如果这些引用点之间的距离非常远,那你就要迫使阅读者的目光在程序里跳来跳去。一个允许任何子程序在任何时间使用任何变量的程序是难于理解的。对于这种程序,你不能只去理解一个子程序,你还必须要理解区域所有使用了相同全局变量的子程序才行,这种程序无论阅读、调试还是修改起来都非常困难。


当代码迁移或重构时,若变量的引用点非常靠近,把相关代码片断重构成独立的子程序就非常容易。


因此,把变量的引用点集中起来除了能降低错误赋值的可能,还能增加代码的可读性,降低代码重构的难度。——《代码大全》


基于上述原因,在定义变量时,应该采用最严格的可见性,然后根据需求扩展变量的作用域:首选将变量局限于某个特定的循环中,然后是局限于某个子程序,其次成为类的私有成员变量,protected 变量,再其次对包可见,最后在迫不得已的情况下再把它作为全局变量。


很不幸 MVVM 架构中,ViewModel 持有的 LiveData 的作用域比想象中的要大,这会带来不可预期的错误。


一开始我是这样定义 LiveData 的:


class SearchViewModel : ViewModel() {
    val liveData1 = MutableLiveData<String>()
    val liveData2 = MutableLiveData<Boolean>()
    val liveData3 = MutableLiveData<Int>()
    val liveData4 = MutableLiveData<Long>()
}


这意味着,在界面可以轻松地拿到 MutableLiveData 的引用,然后改变其值。若这种写法泛滥的话,修改 LiveData 值的代码就会散落在各处,增加阅读代码、修改代码、调试代码的困难。


image.png


图中的红色线条表示界面通过拿到 LiveData 的引用并修改其值。


不可预期的错误是指,当你通过图中的黑线修改 LiveData 值时,它的值可能和你预期不一致,因为还有 N 个别的地方在偷偷的地修改它。


于是乎有了下面这种写法:


class SearchViewModel : ViewModel() {
    private val _liveData1 = MutableLiveData<String>()
    val liveData1: LiveData<String> = _liveData1
    private val liveData2 = MutableLiveData<Boolean>()
    val liveData2: LiveData<Boolean> = _liveData2
    private val liveData3 = MutableLiveData<Int>()
    val liveData3: LiveData<Int> = _liveData3
    private val liveData4 = MutableLiveData<Long>()
    val liveData4: LiveData<Long> = _liveData4
}


只暴露 LiveData 给界面,这样界面就不能擅自修改其值。这样写的好处是所有对 LiveData 的写操作都内聚在 ViewModel 内部,而所有消费 LiveData 的观察者都在 ViewModel 外部。 这降低了维护数据的难度:


image.png


如图所示这就形成了一条“单线数据流”。


为啥单向数据流复杂度就比较低?举个例子,当排查问题时,你想知道到底是哪里修改了 LiveData 的值,在单向数据流的架构中只需要在一个地方打断点就好,因为所有的修改点都收口于此。


上面通过将 MutableLiveda 的作用域收窄简化了数据流动的复杂度。但还有一个复杂度 MVVM 没法化解,因为 LiveData 被定义为 ViewModel 的成员变量,而成员变量的攻击窗口是整个类,因为任何类方法都可以轻松的访问到成员变量。


一个 LiveData 携带着一个 Model,一个 Model 表达这一个界面状态。当新增业务逻辑界面状态发生变化时,首先是在 ViewModel 中新增一个方法以触发业务逻辑,然后在新方法中去修改与该界面状态相关的 LiveData,此时 LiveData 攻击窗口 + 1,处理不好就会变成新增功能导致功能衰退。


纯函数 & 副作用


再从“纯函数 & 副作用”的角度重新审视上述问题:


fun add(a: Int, b: Int){
    return a+b
}


这是一个无副作用的方法,它的结果是可预测的,不会受到除了参数a,b之外其他任何因素的影响。如果输入参数是两个9,返回值必然是18,这个方法执行一万遍结果还是不变,即使方法执行的时候,奥特曼突然出现,结果还是不变。无副作用即可预测,不会发生意料之外的事情。


如果所有的方法都是可以预测的,不会发生意料之外的事情,那该是多美好的一件事情。但往往事与愿违:


var c = 2
fun add(a: Int, b: Int){
    return a+b+c
}


这是一个有副作用的方法,它的结果是不可预测的,因为 c 是一个公共变量,它可能被其他方法访问,其值可能随时发生变化,这导致 add() 的返回值不可预测。


c = 2
add(9,9) // 返回 20
c = 3
add(9,9) // 返回 21


上述代码的执行结果是反直觉的,为啥两次调用 add 的返回值不同?


因为 add() 方法不仅依赖入参还依赖公共变量。你不得不点开 add(),去细究实现细节才能搞懂真相。就好比作者使用晦涩难懂的比喻,为了看明白,不得不逐个百度,再把他们拼凑起来,才能看懂。


除了语义上的难懂,返回值的不可预期也使得程序更容易出错。


更好的写法如下:


fun add(a: Int, b: Int, c: Int){
    return a+b+c
}
var c = 2
add(9, 9, c)


把可能产生副作用的因子作为参数传入,保证方法内部的“无副作用”,这样该方法就可以被安心地调用了。而且该方法也更容易被单元测试了。


但 ViewModel 中所有操纵 LiveData 的函数都不是纯函数,因为 LiveData 是成员变量,这就会发生不可预期的错误。比如测试和你做了同样一段操作,你俩的界面状态就是不一样。因为表面上看执行了相同函数去更新界面状态,但因为它是有副作用的函数,所以不能保证执行两遍就得出相同的结果。这样就为问题的排查设置了重重障碍。


总结


上一篇引入了 MVVM 架构的两个重要概念 ViewModel 以及 LiveData。


通过这一篇的讲述,ViewModel 不仅使得“有免死金牌的业务层”成为可能,也使得跨界面之间的业务逻辑共享以及通信变得轻松。


而 LiveData 不仅使得业务层成为数据持有者以数据驱动刷新界面,还避免了生命周期问题以及内存泄漏风险。


但是 MVVM 引入了新的复杂度,因为更新数据的方法是带有副作用的,由此引起的是不可预期的界面状态。看看下一篇的 MVI 是否能药到病除。


推荐阅读


写业务不用架构会怎么样?(一)


写业务不用架构会怎么样?(二)


写业务不用架构会怎么样?(三)


MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(一)


MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(二)


MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(三)


“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(一)

目录
相关文章
|
4月前
|
存储 测试技术 数据库
谈谈代码:降低复杂度,从放弃三层架构到DDD入门
最近我发现团队某项目的复杂度越来越高(典型的三层架构),具体表现为: - 代码可读性较差:各个服务之间调用复杂,流程不清晰 - 修改某服务业务代码导致大量无关服务的测试用例失败,单个功能开发者很难迅速定位相关问题 - 测试用例特别难编写,需要mock大量数据来拉起整块服务
102 4
谈谈代码:降低复杂度,从放弃三层架构到DDD入门
|
2月前
|
机器学习/深度学习 测试技术 Ruby
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
129 2
|
4月前
|
存储 前端开发
Flutter Provider状态管理---MVVM架构实战
Flutter Provider状态管理—MVVM架构实战 在Flutter中,状态管理是一个非常重要的概念。Flutter Provider是一种状态管理的解决方案,它提供了一种简单,灵活和高效的方法来管理Flutter应用程序中的状态。本文将详细介绍Flutter Provider的使用,以及如何在MVVM架构中使用它。
156 0
|
26天前
|
移动开发 前端开发 数据管理
构建高效Android应用:采用MVVM架构与LiveData的全面指南
在移动开发领域,构建一个既快速又可靠的应用对于开发者来说至关重要。随着Android Jetpack组件的推出,MVVM(Model-View-ViewModel)架构和LiveData已成为实现响应式、可测试且易于维护应用的首选解决方案。本文将深入探讨如何在Android应用中实施MVVM模式,以及如何利用LiveData来优化UI组件的数据更新流程,确保用户界面与业务逻辑之间的高度解耦和流畅交互。
18 4
|
1月前
|
程序员 Python
类的设计奥秘:从代码到架构的科普全解
类的设计奥秘:从代码到架构的科普全解
13 2
|
1月前
|
消息中间件 并行计算 网络协议
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
34 0
|
1月前
|
移动开发 前端开发 C#
MVVM风格架构
MVVM风格架构
28 2
|
2月前
|
设计模式 微服务
从代码到架构,我的技术成长之路
【2月更文挑战第5天】技术是一门不断进步的艺术,我在不断的实践中,通过学习和思考,逐渐领悟到了代码、架构等方面的知识和技能。在这个过程中,我发现技术并不仅仅是一种工具,更是一种思维方式和生活态度。本文将分享我的技术成长历程和所获得的思考。
27 2
|
2月前
|
前端开发 JavaScript API
|
3月前
|
设计模式 存储 前端开发
【各种问题处理】MVC、MVP、MVVM 、MVI、VIPER 架构(设计模式)
【1月更文挑战第13天】【各种问题处理】MVC、MVP、MVVM 、MVI、VIPER 架构(设计模式)