本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

杂乱度

Android 架构演进系列是围绕着杂乱度向前推动的。

软件的首要技能任务是“办理杂乱度” —— 《代码大全》

由于低杂乱度才干下降了解本钱和沟通难度,提高应对改动的灵活性,减少重复劳动,终究提高代码质量。

架构的意图在于“将杂乱度分层”

杂乱度为什么要被分层?

若不分层,杂乱度会在同一层次打开,这样就太 … 杂乱了。

举一个杂乱度不分层的比方:

小李:“你会做什么菜?”

小明:“我会做用土鸡生的土鸡蛋配上切片的西红柿,放点油盐,开战翻炒的西红柿炒蛋。”

听了小明的答复,你还会和他做朋友吗?

小明把不同层次的杂乱度以不恰当的办法揉搓在一起,让人感觉是一种由“没有必要的详细”导致的“难以了解的杂乱”。

小李其实并不关心土鸡蛋的来源、西红柿的切法、添加的佐料、以及烹饪办法。

这样的答复除了难以了解之外,局限性也很大。由于它太详细了!只要把土鸡蛋换成洋鸡蛋、或是西红柿片换成块、或是加点糖、或是换成电磁炉,其间任一要素产生改动,小明就不会做西红柿炒蛋了。

再举个正面的比方,TCP/IP 协议分层模型自下到上界说了五层:

  1. 物理层
  2. 数据链路成
  3. 网络层
  4. 传输层
  5. 应用层

其间每一层的功用都独立且清晰,这样规划的优点是缩小影响面,即单层的变动不会影响其他层。

这样规划的另一个优点是当专心于一层协议时,其他层的技能细节能够不予重视,同一时间只需要重视有限的杂乱度,比方传输层不需要知道自己传输的是 HTTP 仍是 FTP,传输层只需要专心于端到端的传输办法,是树立衔接,仍是无衔接。

有限杂乱度的另一面是“基层的可重用性”。当应用层的协议从 HTTP 换成 FTP 时,其基层的内容不需要做任何更改。

引子

该系列的前三篇结合“查找”这个事务场景,叙述了不运用架构写事务代码会产生的痛点:

  1. 低内聚高耦合的制作:控件的制作逻辑散落在各处,散落在各种 Activity 的子程序中(子程序间彼此耦合),涣散在现在和将来的逻辑中。这样的规划添加了界面改写的杂乱度,导致代码难以了解、简略改出 Bug、难排查问题、无法复用。
  2. 耦合的非粘性通讯:Activity 和 Fragment 经过获取对方引证并互调办法的办法完结通讯。这种通讯办法使得 Fragment 和 Activity 耦合,从而下降了界面的复用度。而且没有一种内建的机制来轻松的完结粘性通讯。
  3. 上帝类:一切细节都在界面被铺开。比方数据存取,网络拜访这些和界面无关的细节都在 Activity 被铺开。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改动源不单一、改动影响规模大。
  4. 界面 & 事务:界面展现和事务逻辑耦合在一起。“界面该长什么样?”和“哪些事件会触发界面重绘?”这两个独立的改动源没有做到重视点别离。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改动源不单一、改动影响规模大、易改出 Bug、界面和事务无法独自被复用。

详细剖析进程能够点击下面的链接:

  1. 写事务不必架构会怎么样?(一)

  2. 写事务不必架构会怎么样?(二)

  3. 写事务不必架构会怎么样?(三)

紧接着又用了三篇叙述了怎么运用 MVP 架构对该事务场景的重构进程。MVP 确实处理了一些问题,但也引进了新问题:

  1. 分层:MVP 最大的奉献在于将界面制作与事务逻辑分层,前者是 MVP 中的 V(View),后者是 MVP 中的 P(Presenter)。分层完结了事务逻辑和界面制作的解耦,让各自愈加单纯,下降了代码杂乱度。
  2. 面向接口通讯:MVP 将事务和界面分层之后,各层之间就需要通讯。通讯经过接口完结,接口把做什么和怎么做别离,使得重视点别离成为或许:接口的持有者只关心做什么,而怎么做留给接口的完结者关心。界面经过事务接口向 Presenter 宣布请求以触发事务逻辑,这使得它不需要关心事务逻辑的完结细节。Presenter 经过 view 层接口回来响应以指导界面改写,这使得它不需要关心界面制作的细节。
  3. 有限的解耦:由于 View 层接口的存在,迫使 Presenter 得了解该把哪个数据塞给哪个 View 层接口。这是一种耦合,Presenter 和这个详细的 View 层接口耦合,较难复用于其他事务。
  4. 有限内聚的界面制作:MVP 并未向界面供给唯一 Model,而是将描绘一个完好界面的 Model 涣散在若干 View 层接口回调中。这使得界面的制作无法内聚到一点,添加了界面制作逻辑保护的杂乱度。
  5. 困难重重的复用:理论上,界面和事务分层之后,各自都愈加单纯,为复用供给了或许性。但不管是事务接口的复用,仍是View层接口的复用都相当别扭。
  6. Presenter 与界面共存亡:这个特性使得 MVP 无法应对反正屏切换的场景。
  7. 无内建跨界面(粘性)通讯机制:MVP 无法优雅地完结跨界面通讯,也未内建粘性通讯机制,得借助第三方库完结。
  8. 生命周期不友好:MVP 并未内建生命周期办理机制,易形成内存走漏、crash、资源糟蹋。

详细剖析进程能够点击下面的链接:

  1. MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(一)

  2. MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(二)

  3. MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(三)

从这一篇开始,试着引进 MVVM 架构的思想进行查找事务场景的重构,看看是否能处理一些痛点。

在重构之前,再介绍下查找的事务场景,该功用示意图如下:

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

事务流程如下:在查找条中输入关键词并同步展现联想词,点联想词跳转查找成果页,若无匹配成果则展现引荐流,回来时查找前史以标签形式横向铺开。点击前史可直接发起查找跳转到成果页。

将查找事务场景的界面做了如下规划:

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

查找页用Activity来承载,它被分成两个部分,头部是常驻在 Activity 的查找条。下面的“查找体”用Fragment承载,它或许呈现三种状况 1.查找前史页 2.查找联想页 3.查找成果页。

Fragment 之间的切换采用 Jetpack 的Navigation。关于 Navigation 详细的介绍能够点击Navigation 组件运用入门 | Android 开发者 | Android Developers

上一篇引进了 MVVM 中两个重要的概念 ViewModel 和 LiveData。它俩调配完结了“生命周期更长的数据持有者”并“以数据驱动的办法”改写界面,使得界面和事务逻辑愈加解耦。

让人又爱又恨的数据重放

查找联想效果如图所示:

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

输入关键词后会主动拉取接口并展现联想词。

这是一个跨界面通讯的场景,由于查找框在 Activity 而联想页是子 Fragment。拉取接口的动作在 Activity 触发,它得把数据传递给 Fragment 进行展现。

在没有粘性通讯加持的情况下,能够有下面两个处理计划:

  1. 发播送:但这里有一个坑,会导致 Fragment 接纳不到播送。(由于发送动作在注册播送之前)
  2. 经过界面跳转时携带参数:会引进参数的序列化和反序列化,略耗性能。

关于上两个处理计划缺点的详细剖析能够点击写事务不必架构会怎么样?(三)

若觉得第二点的性能损耗不是问题的话,它也不是一个通用的计划。当没有产生界面跳转时,就无法运用该计划。比方,当点击联想词时是从联想页跳到成果页,但数据传递却需要从联想页到前史页(点击联想词也视为一次查找行为,得更新前史),此刻界面跳转和数据传递的方向不一致。

粘性通讯是这个场景的最优解。由于粘性意味着数据能够重放,即便在数据发送之后才注册调查者,刚发送的数据照样会从头分发给新的调查者。

对于当时场景来说,Activity 只管拉取联想词并递交给一个粘性的数据持有者,然后触发跳转联想页,待联想页构建结束后才调查已生成的联想词。

输入联想这个场景需要对拉接口做限频,若每次输入改动都拉接口的话,太耗性能及流量了。遂将输入关键词组织成一个流:

etSearch.textChangeFlow { b, input -> input.toString() }
    // 改写查找条
    .onEach { searchViewModel.input(it) } 
    .flowOn(Dispatchers.Main)
    .filter { it.isNotEmpty() }
    .debounce(300) // 300 ms 限频
    .flatMapLatest { 
        flow { 
            // 拉取联想词
            emit(searchViewModel.fetchHint(it)) 
        }  
    } 
    .flowOn(Dispatchers.IO)
    .onEach { hints ->
        // 跳转到联想页
        searchViewModel.setHints(etSearch.text.toString(), hints) 
    }
    .launchIn(lifecycleScope)

上述代码将 EditText 的输入组织成了一个 Flow,这样就能够运用 debounce() 方便地约束拉取联想词的频次。关于流的详细剖析能够点击Kotlin 异步 | Flow 限流的应用场景及原理

同时得为 SearchViewModel 新增和查找联想相关的两个事务动作及对应的数据:

class SearchViewModel : ViewModel() {
    private val repository: SearchRepository = SearchRepository()
    // 联想词数据
    val hintsLiveData = MutableLiveData<List<SearchHint>>()
    // 拉取联想词
    suspend fun fetchHint(keyword: String): List<String> {
        return repository.fetchSearchHint(keyword)
    }
    // 跳转联想页
    fun setHints(keyword: String, hints: List<String>) {
        hintsLiveData.value = hints.map { SearchHint(keyword, it) }
    }
}

其间 Repository 是对拜访数据的封装,这里用到它拜访接口拉取联想词的能力。这个层次结构和 MVP 如出一辙:

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

只不过现在中间的 Presenter 被换成了 ViewModel。

关于数据拜访层的规划详解能够点击MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(二)

最后只需要在联想页中调查粘性数据即可:

class SearchHintFragment : BaseSearchFragment() {
    val searchViewModel: SearchViewModel by activityViewModels<SearchViewModel>()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 调查联想数据并传递给列表的 Adapter
        searchViewModel.hintsLiveData.observe(viewLifecycleOwner){
            hintsAdapter.dataList = it
        }
    }

此处的 onViewCreated() 必定晚于 hintsLiveData 被设值的时刻。粘性的 LiveData 确保了联想词正确地展现。

粘性是 LiveData 自带的特点,它是怎么完结粘性的?

  • LiveData 的值被存储在内部的字段中,直到有更新的值掩盖,所以值是耐久的。
  • 两种场景下 LiveData 会将存储的值分发给调查者。一是值被更新,此刻会遍历一切调查者并分发之。二是新增调查者或调查者生命周期产生改动(至少为 STARTED),此刻只会给单个调查者分发值。
  • LiveData 的调查者会保护一个“值的版本号”,用于判断前次分发的值是否是最新值。该值的初始值是-1,每次更新 LiveData 值都会让版本号自增。
  • LiveData 并不会无条件地将值分发给调查者,在分发之前会经历三道坎:1. 数据调查者是否活泼。2. 数据调查者绑定的生命周期组件是否活泼。3. 数据调查者的版本号是否是最新的。
  • “新调查者”被“老值”通知的现象叫“粘性”。由于新调查者的版本号总是小于最新版号,且添加调查者时会触发一次老值的分发。

关于 LiveData 粘性更详细的源码剖析能够点击LiveData 面试题库、回答、源码剖析

但粘性有时分会产生费事,比方下面这个场景:

“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)
先查找“1”,然后回来到前史页,弹出 toast “新查找词排在最前面”,继续输入“2”,此刻并未触发查找行为,只是进行了联想(2不会被记入前史),但当回来前史,上一次的 toast 再次弹出。

代码完结如下:

class SearchViewModel : ViewModel() {
    val rearrangeLiveData = MutableLiveData<String>()
    val historyLiveData = MutableLiveData<HistoryModel>()
    fun search(keyword: String) {
        ...
        historyLiveData.value = 
            historyLiveData.value?.addHistory(keyword) ?: HistoryModel(mutableListOf(keyword),false)
        // 在触发新查找时提示
        rearrangeLiveData.value = "新查找词汇排在最前面"
    }
}

在 SearchViewModel 中新增了一个数据,它表明 toast 的内容。

然后在前史页调查该数据,并弹出 toast:

class SearchHistoryFragment : BaseSearchFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        searchViewModel.rearrangeLiveData.observe(viewLifecycleOwner){
            Toast.makeText(context,it,Toast.LENGTH_SHORT).show()
        }
    }
}

之所以会弹两次 toast 是由于“前史页被重建+粘性数据”。

当从前史页跳转到联想页时,它的 onDestroyView() 会被调用,所以回来时得从头构建视图,即会触发 onCreateView() -> onViewCreated()。和 toast 相关的 LiveData 正好是在 onViewCreated() 中注册调查者的,从头构建意味着从头注册了一个调查者,又由于 LiveData 是粘性的,所以老数据会分发给新调查者,遂 toast 又弹了一次。

这个 case 中,粘性对前史数据是友好的,由于当时史页重建时,需要从头制作既有的前史查找标签。真实让人又爱又恨的粘性。

针对 LiveData 的粘性,网上有各种处理计划,关于它们孰优孰劣的详细剖析能够点击LiveData 面试题库、回答、源码剖析

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

上面弹 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 值的代码就会散落在各处,添加阅览代码、修正代码、调试代码的困难。

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

图中的红色线条表明界面经过拿到 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 外部。 这下降了保护数据的难度:

“无架构”和“MVP”都救不了业务代码,MVVM能力挽狂澜?(二)
如图所示这就形成了一条“单线数据流”。

为啥单向数据流杂乱度就比较低?举个比方,当排查问题时,你想知道到底是哪里修正了 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能力挽狂澜?(一)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。