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

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

复杂度


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


软件的首要技术使命是“管理复杂度” —— 《代码大全》


因为低复杂度才能降低理解成本和沟通难度,提升应对变更的灵活性,减少重复劳动,最终提高代码质量。


架构的目的在于“将复杂度分层”


复杂度为什么要被分层?


若不分层,复杂度会在同一层次展开,这样就太 ... 复杂了。


举一个复杂度不分层的例子:


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


小明:“我会做用土鸡生的土鸡蛋配上切片的番茄,放点油盐,开火翻炒的番茄炒蛋。”


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


小明把不同层次的复杂度以不恰当的方式揉搓在一起,让人感觉是一种由“没有必要的具体”导致的“难以理解的复杂”。


小李其实并不关心土鸡蛋的来源、番茄的切法、添加的佐料、以及烹饪方式。


这样的回答除了难以理解之外,局限性也很大。因为它太具体了!只要把土鸡蛋换成洋鸡蛋、或是番茄片换成块、或是加点糖、或是换成电磁炉,其中任一因素发生变化,小明就不会做番茄炒蛋了。


再举个正面的例子,TCP/IP 协议分层模型自下到上定义了五层:


  1. 物理层


  1. 数据链路成


  1. 网络层


  1. 传输层


  1. 应用层


其中每一层的功能都独立且明确,这样设计的好处是缩小影响面,即单层的变动不会影响其他层。


这样设计的另一个好处是当专注于一层协议时,其余层的技术细节可以不予关注,同一时间只需要关注有限的复杂度,比如传输层不需要知道自己传输的是 HTTP 还是 FTP,传输层只需要专注于端到端的传输方式,是建立连接,还是无连接。


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


引子


该系列的前三篇结合“搜索”这个业务场景,讲述了不使用架构写业务代码会产生的痛点:


  1. 低内聚高耦合的绘制:控件的绘制逻辑散落在各处,散落在各种 Activity 的子程序中(子程序间相互耦合),分散在现在和将来的逻辑中。这样的设计增加了界面刷新的复杂度,导致代码难以理解、容易改出 Bug、难排查问题、无法复用。


  1. 耦合的非粘性通信:Activity 和 Fragment 通过获取对方引用并互调方法的方式完成通信。这种通信方式使得 Fragment 和 Activity 耦合,从而降低了界面的复用度。并且没有一种内建的机制来轻松的实现粘性通信。


  1. 上帝类:所有细节都在界面被铺开。比如数据存取,网络访问这些和界面无关的细节都在 Activity 被铺开。导致 Activity 代码不单纯、高耦合、代码量大、复杂度高、变化源不单一、改动影响范围大。


  1. 界面 & 业务:界面展示和业务逻辑耦合在一起。“界面该长什么样?”和“哪些事件会触发界面重绘?”这两个独立的变化源没有做到关注点分离。导致 Activity 代码不单纯、高耦合、代码量大、复杂度高、变化源不单一、改动影响范围大、易改出 Bug、界面和业务无法单独被复用。


详细分析过程可以点击下面的链接:


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


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


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


紧接着又用了三篇讲述了如何使用 MVP 架构对该业务场景的重构过程。MVP 的确解决了一些问题,但也引入了新问题:


  1. 分层:MVP 最大的贡献在于将界面绘制与业务逻辑分层,前者是 MVP 中的 V(View),后者是 MVP 中的 P(Presenter)。分层实现了业务逻辑和界面绘制的解耦,让各自更加单纯,降低了代码复杂度。


  1. 面向接口通信:MVP 将业务和界面分层之后,各层之间就需要通信。通信通过接口实现,接口把做什么和怎么做分离,使得关注点分离成为可能:接口的持有者只关心做什么,而怎么做留给接口的实现者关心。界面通过业务接口向 Presenter 发出请求以触发业务逻辑,这使得它不需要关心业务逻辑的实现细节。Presenter 通过 view 层接口返回响应以指导界面刷新,这使得它不需要关心界面绘制的细节。


  1. 有限的解耦:因为 View 层接口的存在,迫使 Presenter 得了解该把哪个数据塞给哪个 View 层接口。这是一种耦合,Presenter 和这个具体的 View 层接口耦合,较难复用于其他业务。


  1. 有限内聚的界面绘制:MVP 并未向界面提供唯一 Model,而是将描述一个完整界面的 Model 分散在若干 View 层接口回调中。这使得界面的绘制无法内聚到一点,增加了界面绘制逻辑维护的复杂度。


  1. 困难重重的复用:理论上,界面和业务分层之后,各自都更加单纯,为复用提供了可能性。但不管是业务接口的复用,还是View层接口的复用都相当别扭。


  1. Presenter 与界面共存亡:这个特性使得 MVP 无法应对横竖屏切换的场景。


  1. 无内建跨界面(粘性)通信机制:MVP 无法优雅地实现跨界面通信,也未内建粘性通信机制,得借助第三方库实现。


  1. 生命周期不友好:MVP 并未内建生命周期管理机制,易造成内存泄漏、crash、资源浪费。


详细分析过程可以点击下面的链接:


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


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


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


从这一篇开始,试着引入 MVVM 架构的思想进行搜索业务场景的重构,看看是否能解决一些痛点。


在重构之前,再介绍下搜索的业务场景,该功能示意图如下:


image.png


https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/84508632b7bc488f9d5ae0386090e567~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?


业务流程如下:在搜索条中输入关键词并同步展示联想词,点联想词跳转搜索结果页,若无匹配结果则展示推荐流,返回时搜索历史以标签形式横向铺开。点击历史可直接发起搜索跳转到结果页。


将搜索业务场景的界面做了如下设计:


image.png


搜索页用Activity来承载,它被分成两个部分,头部是常驻在 Activity 的搜索条。下面的“搜索体”用Fragment承载,它可能出现三种状态 1.搜索历史页 2.搜索联想页 3.搜索结果页。


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


有免死金牌的业务层


在使用 MVP 重构搜索业务时,存在“Presenter 与界面共存亡”的问题,即 Presenter 在 Activity 实例内部构建,遂其生命周期与 Activity 同步。当 Activity 销毁重建时,Presenter 也跟着一起销毁重建。当 Presenter 初始化时存在耗时操作时,这样的从头来过就很浪费资源。(详细分析可以点击MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?


MVVM 中的 VM 即ViewModel,它是与 MVP 中 Presenter 相对应的概念,即业务逻辑层(它在此基础上又拓展出新的作用),它的引入解决了这个痛点。


ViewModel 是 JetPack 提供的一个类:


public abstract class ViewModel {
    /**
     * Construct a new ViewModel instance.
     * You should never manually construct a ViewModel outside of a
     * {@link ViewModelProvider.Factory}.
     */
    public ViewModel() {}
}


ViewModel 虽然提供了公有的构造方法,但注解提示说“永远不要手动构建 ViewModel 实例,而是得通过ViewModelProvider.Factory


public interface Factory {
    <T extends ViewModel> T create(@NonNull Class<T> modelClass);
}


Factory 是一个接口,是对如何构建 ViewModel 的一个抽象。


之所以不允许直接构建而是必须通过 Factory,是因为系统希望掌控 ViewModel 的实例构建,在内部帮助开发者构建 ViewModel 实例。若把 ViewModel 的构建方法放开,则上层可能出现各种各样自定义的构建方法(比如在构造方法中出入不同的参数)。


那为啥系统要掌控 ViewModel 实例的构建?


因为系统对 ViewModel 实例的存取做了特殊处理。


ViewModel 通常是这样声明的:


class SearchViewModel(
    private val repository: SearchRepository
): ViewModel() { }
class SearchFactory(val repository: SearchRepository): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return SearchViewModel(searchRepository) as T
    }
}


其中的 Repository 是对访问数据的封装,比如网络请求,关于它的详细解释可以点击MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?


而 ViewModel 通常在 Activity 中这样被构建:


class TemplateSearchActivity : AppCompatActivity() {
    private val searchViewModel by lazy {
        ViewModelProvider(
            this, 
            SearchFactory(SearchRepository())).get(SearchViewModel::class.java)
    }
}


构建 Presenter 是直接在 Activity 中 new,而构建 ViewModel 是通过ViewModelProvider().get():


public class ViewModelProvider {
    // ViewModel 实例商店
    private final ViewModelStore mViewModelStore;
    private Factory mFactory;
    public <T extends ViewModel> T get(String key, Class<T> modelClass) {
        // 从商店获取 ViewModel实例
        ViewModel viewModel = mViewModelStore.get(key);
        // 若 ViewModel 匹配指定类型则直接返回
        if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
        } 
        ...
        // 若商店无 ViewModel 实例 则通过 Factory 构建
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
        } else {
            viewModel = (mFactory).create(modelClass);
        }
        // 将 ViewModel 实例存入商店
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
}


ViewModelProvider是一个获取 ViewModel 实例的工具类,它屏蔽了通过访问ViewModelStore获取 ViewModel 实例的细节。


ViewModelStore 是真正存在 ViewModel 实例的地方:


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


ViewModelStore内部持有一个 HashMap,这是 ViewModel 实例的最终存放点。


而 ViewModelStore 的实例是通过ViewModelStoreOwner获取:


public class ViewModelProvider {
    // ViewModel 实例商店
    private final ViewModelStore mViewModelStore;
    // 构造 ViewModelProvider 时需传入 ViewModelStoreOwner 实例
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        // 通过 ViewModelStoreOwner 获取 ViewModelStore 
        this(owner.getViewModelStore(), factory);
    }
    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
}


ViewModelStoreOwner实例又存储在哪?


// 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;
            ...
        }
}


Activity 就是ViewModelStoreOwner实例,且持有ViewModelStore实例。


但当横竖屏切换 Activity 销毁重建时,作为成员变量的 ViewModelStore 依然会被销毁,为了避免它被重建,在配置发生变化时 onRetainNonConfigurationInstance(),ViewModelStore 实例还会被寄存在一个静态类NonConfigurationInstances中,这样在恢复时,就可以从中恢复 ViewModelStore 实例。


最终的持有链如下:


Activity {
    ViewModelStore {
        HashMap<String, ViewModel>
    }
    NonConfigurationInstances { ViewModelStore }
}


阶段性总结:


  • ViewModel 的构建被抽象为ViewModelProvider.Factory


  • ViewModel 的实例被存储ViewModelStore中的 HashMap 结构中 ViewModelStore 的构建被抽象为ViewModelStoreOwner,Activity 和 Fragment 都实现了该接口且持有了 ViewModelStore 的实例,在配置发生变化时 onRetainNonConfigurationInstance(),ViewModelStore 实例还会被寄存在一个静态类NonConfigurationInstances中,这样在恢复时,就可以从中恢复 ViewModelStore 实例。


  • ViewModel 的获取通过 ViewModelProvider 实现,它屏蔽了通过 ViewModelProvider.Factory 构建以及通过 ViewModelStore 缓存 ViewModel 实例的细节。


  • ViewModel 的生命周期会在 Activity.onDestroy() 结束。此时 Activity 会清理其持有的 ViewModelStore 中的所有 ViewModel 实例。


  • 这套存储机制使得 ViewModel 在 Activity 配置发生变化被销毁重建时获得了免死金牌,以保证不会重新触发业务逻辑。


数据持有者 & 数据驱动


假设 Presenter 也套用 ViewModel 这套构建机制,是否就能解决横竖屏场景下的所有问题?


不能!


解决横竖屏问题需要做到两点:


  1. 比 Activity 生命周期更长的业务逻辑层。


  1. 业务逻辑层持有数据并且具备数据重放能力。


即使 Presenter 做到了更长的生命周期也只是解决了第一个问题。因为 Presenter 它不是一个数据持有者,更别提数据重放了。


引用上一篇关于 MVP 数据流动的示意图:


image.png


Presenter 只持有了 Repository,它并不持有数据,即不存在一个叫 data 的成员变量。从 Repository 获取的数据是直接在业务接口中传递给了 View 层接口的。


也就是说,当触发了一个业务动作后,数据发生了一次从 Repository 到 Presenter 再到界面的流动。整个流动的过程中并没有一个地方把数据存下来。所以 Presenter 不是一个数据持有者


既然 Presenter 不持有数据,那它也无法把上次流过的数据进行重放,即重新发送给界面。那在 MVP 架构中,当 Activity 销毁重建时,如何恢复界面刚才的样子?答案是“无法恢复!”,只能重新触发一遍业务动作,比如重新请求网络,一切从头再来!


ViewModel 的出现同时把上述两个问题都解决了,总结为一句话即是“ViewModel 是生命周期更长的数据持有者。


ViewModel 借助于LiveData的帮助实现了数据持有者的效果。


LiveData 也是 JetPack 的一员。它是能感知生命周期的,可观察的,粘性的,数据持有者。LiveData 用于以“数据驱动”方式更新界面。


关于 LiveData 的详细讲解可以点击LiveData 面试题库、解答、源码分析


在 MVP 架构中界面的刷新是命令式编程,即界面刷新是通过手动调用方法实现的。


命令式编程会产生耦合。


手动调用某个方法的前提是得先获取对应的对象,在 MVP 架构中,描述界面如何绘制的对象叫“View 层接口”,Presenter 得先持有 View 层接口的实例,然后在内部根据业务逻辑手动调用相应的 View 层接口,即 Presenter 得知道要把哪个数据塞给哪个 View 层接口。这使得 Presenter 和 View 层接口耦合,或者说业务和界面耦合。耦合导致复用困难。详细分析可以点击MVP 架构最终审判 —— MVP 解决了哪些痛点,又引入了哪些坑?(三)


更加解耦的方式是数据驱动:让业务层只操纵数据,界面通过观察数据的方式实现刷新。


这里的数据指的是 MXX 架构中的 M,即 Model。


如此一来业务层不再持有任何和界面相关的东西,只和数据有关。不同的界面可以以任何喜欢的方式组合使用业务层提供的数据。(MVP 做不到这点,因为数据是通过 View 层接口给出去,组合使用略困难)


面向业务抽象Model


下面就以搜索条为例,看看用 MVVM 架构重构之后会是什么样子。


搜索条的业务场景如下:


image.png


https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eca5fe3c859d477bacea8f02ab02c625~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?


当输入框键入内容后,显示X按钮并高亮搜索按钮。点击搜索跳转到搜索结果页,同时搜索条拉长并隐藏搜索按钮。点击X时清空输入框并从搜索结果页返回,搜索条还原。


根据业务逻辑为 ViewModel 添加一系列动作及数据:


class SearchViewModel(private val searchRepository: SearchRepository) : ViewModel() {
    // 业务数据持有者
    val initLiveData = MutableLiveData<Boolean>()
    val keywordLiveData = MutableLiveData<String>()
    val searchLiveData = MutableLiveData<String>()
    val clearLiveData = MutableLiveData<Boolean>()
    val backToHistoryLiveData = MutableLiveData<Boolean>()
    // 业务动作
    fun init() { initLiveData.value = true }
    fun search(keyword: String) { searchLiveData.value = keyword }
    fun clear() { clearLiveData.value = true }
    fun input(keyword: String) { keywordLiveData.value = keyword }
    fun backToHistory(){ backToHistoryLiveData.value = true }
}


每一个函数代表着一个业务逻辑,并有与之对应的一个业务数据(以 LiveData 形式表达)


界面通过观察数据来更新视图:


class TemplateSearchActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(contentView)
        searchViewModel.init()
        observerData()
        initView()
    }
    // 观察数据并刷新界面
    private fun observerData() {
        searchViewModel.initLiveData.observe(this, Observer {
            if (it) {
                tvSearch.apply {
                    isEnabled = false
                    textColor = "#484951"
                }
                ivClear.visibility = gone
                KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
            }
        })
        searchViewModel.searchLiveData.observe(this, Observer {
           searchAndHideKeyboard(it)
        })
        searchViewModel.keywordLiveData.observe(this, Observer {
            if (it.isNotEmpty()) {
                tvSearch.textColor = "#F2F4FF"
                tvSearch.isEnabled = true
                ivClear.visibility = visible
            } else {
                tvSearch.textColor = "#484951"
                tvSearch.isEnabled = false
                ivClear.visibility = gone
            }
        })
        searchViewModel.clearLiveData.observe(this, Observer {
            if(it){
                etSearch.text = null
                etSearch.requestFocus()
                KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
                backToHistory()
            }
        })
        searchViewModel.backToHistoryLiveData.observe(this, Observer {
            backToHistory()
        })
    }
    private fun initView() {
        etSearch.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?,s: Int, c: Int, a: Int) {}
            override fun onTextChanged(char: CharSequence?, s: Int, b: Int, c: Int) {
                val input = char?.toString() ?: ""
                // 向 ViewModel 发起业务动作
                searchViewModel.input(input) 
            }
            override fun afterTextChanged(s: Editable?) {}
        })
        etSearch.setOnEditorActionListener { v, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                val input = etSearch?.text?.toString() ?: ""
                if (input.isNotEmpty()) {
                    // 向 ViewModel 发起业务动作
                    searchViewModel.search(input) 
                }
                true
            } else false
        }
        tvSearch.setOnClickListener {
            // 向 ViewModel 发起业务动作
            searchViewModel.search(etSearch.text.toString()) 
        }
        ivClear.setOnClickListener {
            // 向 ViewModel 发起业务动作
            searchViewModel.clear() 
        }
        etSearch.setOnTouchListener { v, event ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                // 向 ViewModel 发起业务动作
                searchViewModel.backToHistory() 
            }
            false
        }
    }
}


这样的写法颇有脱裤子放屁的感觉,还引入了额外的复杂度 ViewModel。但这裤子脱得还是有价值的:


  1. 业务逻辑和界面展示分离:这使得界面展示和业务逻辑可以独立的变化而不会相互影响。(这一点MVP也可以做到)


  1. 更新视图的逻辑不再散落各处:界面通过观察数据较为集中地进行更新。但遗憾的是,同一个视图的更新逻辑还是会散落在不同数据的观察者中。


现在看来和上一篇用 MVP 重构的效果没任何两样,反而因为引入了 ViewModel 和 LiveData 增加了复杂度。


MVVM 的好处当然不止于此,后续章节会慢慢展开。(这一小节只是先展示 MVVM 的概貌)


面向界面抽象Model


用一张图来表达下上一小节 MVVM 的复杂度:


image.png


它完成了界面展示与业务逻辑分离,但控件的刷新逻辑散落在不同数据的观察者中,依然无法将“界面应该长什么样子?”这个问题内聚于一点。


之所以会这样是因为“错误的 Model 抽象”。


上述代码是以业务逻辑作为抽象 Model 的依据。比如与“返回历史页”对应的数据是一个布尔值,用来表示是否触发了返回。这使得 Model 和业务强绑定,业务一变,原先的数据就没用了。Model 应该和业务无关,Model 应该只表达界面该长成什么样子


按照这个思路,MVVM 的 Model 应该做如下改造:


class SearchViewModel : ViewModel() {
    // 搜索按钮颜色
    val searchButtonColorLiveData = MutableLiveData<String>()
    // 搜索按钮是否可点击
    val searchButtonClickableLiveData = MutableLiveData<Boolean>()
    // 搜索按钮是否可见
    val searchButtonVisibilityLiveData = MutableLiveData<Boolean>()
    // 清除按钮是否显示
    val clearButtonVisibilityLiveData = MutableLiveData<Boolean>()
    // 搜索条是否拉深
    val searchBarStretchLiveData = MutableLiveData<Boolean>()
    // 键盘是否展示
    val keyboardLiveData = MutableLiveData<Boolean>()
    // 跳转到搜索结果页
    val gotoSearchLiveData = MutableLiveData<String>()
    // 关键词
    val keywordLiveData = MutableLiveData<String>()
    // 从结果页返回
    val popupLiveData = MutableLiveData<Boolean>()
    fun init() {
        keyboardLiveData.value = true
        searchButtonColorLiveData.value = "#484951"
        searchButtonClickableLiveData.value = false
        searchButtonVisibilityLiveData.value = true
        searchBarStretchLiveData.value = false
    }
    fun search(keyword: String) {
        gotoSearchLiveData.value = keyword
        keyboardLiveData.value = false
        searchBarStretchLiveData.value = true
    }
    fun clear() {
        clearButtonVisibilityLiveData.value = true
        searchButtonClickableLiveData.value = false
        searchButtonColorLiveData.value = "#484951"
        keywordLiveData.value = ""
    }
    fun input(keyword: String) {
        if (keyword.isNullOrEmpty()) {
            searchButtonColorLiveData.value = "#484951"
            searchButtonVisibilityLiveData.value = true
            searchButtonClickableLiveData.value = false
            clearButtonVisibilityLiveData.value = false
        } else {
            searchButtonColorLiveData.value = "#F2F4FF"
            searchButtonVisibilityLiveData.value = true
            searchButtonClickableLiveData.value = true
            clearButtonVisibilityLiveData.value = true
        }
    }
    fun popUp() {
        searchButtonClickableLiveData.value = false
        searchButtonColorLiveData.value = "#484951"
        searchButtonVisibilityLiveData.value = true
        clearButtonVisibilityLiveData.value = false
        keywordLiveData.value = ""
        searchBarStretchLiveData.value = false
        popupLiveData.value = true
    }
}


以控件的某个属性作为抽象 Model 的依据,不同的业务逻辑函数会修改相应的控件属性 Model,界面再观察 Model。


绘制界面逻辑也相应地做如下修改:


// TemplateSearchActivity.kt
private fun observeData() {
    searchViewModel.keyboardLiveData.observe(this){
        if(it) KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
        else KeyboardUtils.hideSoftInput(etSearch)
    }
    searchViewModel.clearButtonVisibilityLiveData.observe(this){
        ivClear.visibility = if(it) visible else gone
    }
    searchViewModel.searchButtonVisibilityLiveData.observe(this){
        tvSearch.visibility = if(it) visible else gone
    }
    searchViewModel.searchButtonColorLiveData.observe(this) {
        tvSearch.textColor = it
    }
    searchViewModel.searchButtonClickableLiveData.observe(this){
        tvSearch.isEnabled = it
    }
    searchViewModel.searchBarStretchLiveData.observe(this){
        vInputBg.apply {
            if (it) end_toEndOf = parent_id
            else end_toStartOf = ID_SEARCH
        }
    }
    searchViewModel.gotoSearchLiveData.observe(this){
        findNavController(NAV_HOST_ID.toLayoutId()).navigate(
            R.id.action_to_result,
            bundleOf("keywords" to it)
        )
    }
    searchViewModel.keywordLiveData.observe(this){
        if(it.isNullOrEmpty()) {
            etSearch.text = null
            etSearch.requestFocus()
            KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
        }
    }
    searchViewModel.popupLiveData.observe(this){
        if(it){
            findNavController(NAV_HOST_ID.toLayoutId()).popBackStack()
        }
    }
}


更内聚的Model


上面的这次重构解决了 Model 和业务强耦合的问题,但那个老问题依然没有得到解决,甚至还加重了,即刷新界面的逻辑散落在更多的数据观察者中,无法形成对界面绘制统一的认知。


用一张图表达下此时 MVVM 的复杂度:


image.png


看上去挺复杂的。之所以会这样是因为数据源不单一。比如搜索按钮应该长什么样用了三个 Model 来表示:


  1. searchButtonColorLiveData


  1. searchButtonClickableLiveData


  1. searchButtonVisibilityLiveData


当 UI 发生变更,搜索按钮要添加一个渐变的背景色时,是不是还要新增一个 Model?

这样设计的话复杂度就会陡增。


当前按钮有2种颜色,2种点击状态,2种可见状态。当把这三个维度分别用三个 Model 来表达时,意味着它们可以不受控制地独立变化,进而形成 2 * 2 * 2 = 8 种排列组合。但其中只有 3 种组合是符合预期的。如何保证在改这块代码时不生成错误的排列组合(界面状态)?


进一步,搜索按钮的可见状态是和搜索条的长度联动的,即只有当搜索条拉长时按钮才不可见。如果处理不好就会产生如下的界面状态不一致:


image.png


另外,清空按钮也会和搜索按钮的颜色联动。


更好的设计应该是用一个 Model 表达所有相关的界面状态


data class SearchBarModel(
    val searchButtonColor: String,// 搜索按钮颜色
    val isSearchButtonClickable: Boolean, // 搜索按钮是否可点击
    val isSearchBarStretch: Boolean, // 搜索条是否拉升
    val isClearShow: Boolean, // 是否展示清空按钮
)


对应的 ViewModel 做相应的修改:


class SearchViewModel : ViewModel() {
    // 搜索按钮颜色
    val searchBarLiveData = MutableLiveData<SearchBarModel>()
    // 键盘是否展示
    val keyboardLiveData = MutableLiveData<Boolean>()
    // 跳转到搜索结果页
    val gotoSearchLiveData = MutableLiveData<String>()
    // 关键词
    val keywordLiveData = MutableLiveData<String>()
    // 从结果页返回
    val popupLiveData = MutableLiveData<Boolean>()
    fun init() {
        keyboardLiveData.value = true
        searchBarLiveData.value = SearchBarModel(
            "#484951",
            false,
            false,
            false
        )
    }
    fun search(keyword: String) {
        gotoSearchLiveData.value = keyword
        keyboardLiveData.value = false
        searchBarLiveData.value = SearchBarModel(
            "#484951",
            false,
            true,
            true
        )
    }
    fun clear() {
        keywordLiveData.value = ""
        searchBarLiveData.value = SearchBarModel(
            "#484951",
            false,
            false,
            false
        )
    }
    fun input(keyword: String) {
        if (keyword.isNullOrEmpty()) {
            searchBarLiveData.value = SearchBarModel(
                "#484951",
                false,
                false,
                false
            )
        } else {
            searchBarLiveData.value = SearchBarModel(
                "#F2F4FF",
                true,
                false,
                true
            )
        }
    }
    fun popUp() {
        keywordLiveData.value = ""
        popupLiveData.value = true
        searchBarLiveData.value = SearchBarModel(
            "#484951",
            false,
            false,
            false
        )
    }
}


当任何一个影响搜索条状态变化的事件发生时,你都得构建一个 SearchBarModel 并为其中的四个参数赋值。这迫使你将所有的状态都考虑在内,避免遗留。这样的设计极大的降低了代码的复杂度。


界面绘制代码也得做相应修改:


// TemplateSearchActivity.kt
private fun observeData() {
    searchViewModel.searchBarLiveData.observe(this) { model->
        ivClear.visibility = if(model.isClearShow) visible else gone
        tvSearch.apply {
            textColor = model.searchButtonColor
            visibility = if(model.isSearchBarStretch) gone else visible
            isEnable = model.isSearchButtonClickable
        }
        vInputBg.apply {
            if (model.isSearchBarStretch) end_toEndOf = parent_id
            else end_toStartOf = ID_SEARCH
        }
    }
}


如此一来绘制界面的代码也更加内聚了,所有关于搜索按钮长什么样的代码都内聚在一个数据观察者回调中,后期修改搜索按钮样式的时候,不至于要满 Activity 地找控件。


再用一张图看下简化后的复杂度:


image.png


LiveData 数量少了,业务逻辑和 LiveData 交互的逻辑也少了。


总结


这一篇主要引入了 MVVM 架构的两个重要概念 ViewModel 以及 LiveData。前者使得“有免死金牌的业务层”成为可能,后者使得业务层成为数据持有者,并以数据驱动刷新界面。


下一篇会继续讲述 MVVM 并用实战代码展示它的痛点。尽请期待~


推荐阅读


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


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


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


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


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


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


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


目录
相关文章
|
5天前
|
前端开发
MVVM LiveData+DataBinding+Lifecycle+ViewModel架构
MVVM LiveData+DataBinding+Lifecycle+ViewModel架构
13 1
|
6天前
|
存储 前端开发 Java
Android应用开发中的MVP架构模式实践
【5月更文挑战第5天】随着移动应用开发的复杂性增加,传统的MVC(Model-View-Controller)架构在应对大型项目时显得笨重且不灵活。本文将探讨一种更适应现代Android应用开发的架构模式——MVP(Model-View-Presenter),并展示如何在Android项目中实现该模式以提升代码的可维护性和可测试性。通过对比分析MVP与传统MVC的差异,以及提供一个实际案例,读者将能深入了解MVP的优势和实施步骤。
|
11天前
|
前端开发 测试技术 数据处理
安卓开发中的MVP架构模式深度解析
【4月更文挑战第30天】在移动应用开发领域,模型-视图-呈现器(Model-View-Presenter, MVP)是一种广泛采用的架构模式。它旨在通过解耦组件间的直接交互来提高代码的可维护性和可测试性。本文将深入探讨MVP在安卓开发中的应用,揭示其如何促进代码的模块化,提升用户界面的响应性,并简化单元测试过程。我们将从理论概念出发,逐步过渡到实践案例,为读者提供一套行之有效的MVP实施策略。
|
12天前
|
前端开发 开发者
【专栏】BEM(Block-Element-Modifier)是一种前端命名规范和架构方法,旨在创建清晰、可维护的代码结构。
【4月更文挑战第29天】BEM(Block-Element-Modifier)是一种前端命名规范和架构方法,旨在创建清晰、可维护的代码结构。它包括Block(独立功能单元)、Element(Block的子元素)和Modifier(表示状态或变体)。BEM的特点包括命名一致性、模块化设计、清晰结构和可复用性,适用于代码组织、样式管理、组件化开发和团队协作。虽然命名较长和学习成本是其局限性,但BEM在提升代码质量和效率方面具有显著优势,是前端开发的重要工具。
|
13天前
|
运维 监控 Serverless
【专栏】无服务器架构,一种云计算模型,让开发者专注编写代码而不必管理服务器(Serverless)
【4月更文挑战第28天】无服务器架构,一种云计算模型,让开发者专注编写代码而不必管理服务器。它基于事件驱动,自动扩展资源并按需计费。优势包括缩短开发周期、优化资源利用、降低成本、提高可用性及简化维护。然而,冷启动延迟、调试困难、性能监控、安全性和学习曲线等挑战仍需解决。随着技术进步,无服务器架构将在科技发展中发挥更大作用。
|
19天前
|
缓存 监控 算法
Python性能优化面试:代码级、架构级与系统级优化
【4月更文挑战第19天】本文探讨了Python性能优化面试的重点,包括代码级、架构级和系统级优化。代码级优化涉及时间复杂度、空间复杂度分析,使用内置数据结构和性能分析工具。易错点包括过度优化和滥用全局变量。架构级优化关注异步编程、缓存策略和分布式系统,强调合理利用异步和缓存。系统级优化则涵盖操作系统原理、Python虚拟机优化和服务器调优,需注意监控系统资源和使用编译器加速。面试者应全面理解这些层面,以提高程序性能和面试竞争力。
17 1
Python性能优化面试:代码级、架构级与系统级优化
|
20天前
|
设计模式 前端开发 数据库
构建高效Android应用:使用Jetpack架构组件实现MVVM模式
【4月更文挑战第21天】 在移动开发领域,构建一个既健壮又易于维护的Android应用是每个开发者的目标。随着项目复杂度的增加,传统的MVP或MVC架构往往难以应对快速变化的市场需求和复杂的业务逻辑。本文将探讨如何利用Android Jetpack中的架构组件来实施MVVM(Model-View-ViewModel)设计模式,旨在提供一个更加模块化、可测试且易于管理的代码结构。通过具体案例分析,我们将展示如何使用LiveData, ViewModel, 和Repository来实现界面与业务逻辑的分离,以及如何利用Room数据库进行持久化存储。最终,你将获得一个响应迅速、可扩展且符合现代软件工
24 0
|
1月前
|
消息中间件 存储 缓存
大厂MVP技术JAVA架构师培养
这是一个很强悍的架构师涨薪计划课程,课程由专家级MVP讲师进行教学,分为是一个章节进行分解式面试及讲解,不仅仅是面试,更像是一个专业的架构师研讨会课程。课程内容从数据结构与算法、Spring Framwork、JVM原理、 JUC并发编程、消息队列、存储、缓存与搜索、服务治理与分布式、云原生、业务场景与项目架构设计等业务场景,进行全方位的讨论与指导,非常有提升性。
24 0
|
1月前
|
存储 设计模式 前端开发
构建高效安卓应用:Jetpack MVVM 架构的实践之路
【4月更文挑战第9天】 在移动开发的迅猛浪潮中,Android 平台以其开放性和灵活性受到开发者青睐。然而,随着应用复杂度的不断增加,传统的开发模式已难以满足快速迭代和高质量代码的双重要求。本文将深入探讨 Jetpack MVVM 架构模式在 Android 开发中的应用实践,揭示如何通过组件化和架构设计原则提升应用性能,实现数据驱动和UI分离,进而提高代码可维护性与测试性。我们将从理论出发,结合具体案例,逐步展开对 Jetpack MVVM 架构的全面剖析,为开发者提供一条清晰、高效的技术实施路径。
|
1月前
|
移动开发 前端开发 数据管理
构建高效Android应用:采用MVVM架构与LiveData的全面指南
在移动开发领域,构建一个既快速又可靠的应用对于开发者来说至关重要。随着Android Jetpack组件的推出,MVVM(Model-View-ViewModel)架构和LiveData已成为实现响应式、可测试且易于维护应用的首选解决方案。本文将深入探讨如何在Android应用中实施MVVM模式,以及如何利用LiveData来优化UI组件的数据更新流程,确保用户界面与业务逻辑之间的高度解耦和流畅交互。
21 4