如何像Pro一样在Android上使用Model-View-ViewModel

2020年12月30日10:52:03 发表评论 30 次浏览

本文概述

我在本文中的目的是解释为什么在某些情况下, 关于GUI架构的表示逻辑, Model-View-ViewModel架构模式在关注点上存在非常尴尬的分离。

我们将探讨MVVM的两种变体(不只是一种实现方式), 以及根据项目需求而选择一个变体而不是另一个变体的原因。

MVVM与MVP / MVC?

在周日的现场问答环节中, 我最常见的问题很可能是这样的:

MVVM与MVP / MVC?

每当我被问到这个问题时, 我都会迅速强调一个想法, 即没有一个GUI架构在所有情况下都能发挥出色的作用。

为什么, 你可能会问?给定应用程序的最佳体系结构(或至少是一个不错的选择)在很大程度上取决于手头的要求。

让我们简要考虑一下这个词是什么要求实际上意味着:

  • 你的用户界面有多复杂?简单的UI通常不需要复杂的逻辑来协调它, 而复杂的UI可能需要广泛的逻辑和细粒度的控件才能平稳地工作。
  • 你在乎多少测试?一般而言, 与框架和操作系统紧密相关的类(尤其是用户界面)需要额外的工作进行测试。
  • 你希望提升多少可重用性和抽象性?如果你想在不同平台上共享应用程序的后端, 域甚至表示逻辑, 该怎么办?
  • 你是天生的吗务实, 完美主义者, 懒, 或上述所有这些都是在不同的时间, 不同的情况?

我很想写一篇文章, 详细讨论MVVM如何针对上述要求和关注事项进行工作。不幸的是, 你们中的某些人可能会误以为只有一种方法可以制作MVVM。

取而代之的是, 我将讨论两种不同的MVVM总体思想方法, 这些方法具有非常明显的优缺点。但是首先, 让我们从总体思路开始。

不可引用你的视图类

对于无法阅读古英语的朋友:"你可能没有引用视图类。"

除了使用名称ViewModel(如果类已满, 它本身会造成混淆)逻辑), MVVM体系结构的一条铁腕规则是你永远都不能从ViewModel引用View。

现在, 第一个混乱的领域可能来自"参考"这个词, 我将使用几种不同的术语来重申这一点:

  • 你的ViewModel可能不具有对任何View的任何引用(成员变量, 属性, 可变/不可变字段)
  • 你的ViewModel可能不依赖于任何视图
  • 你的ViewModel可能无法直接与你的视图对话

现在, 在Android平台上, 执行此规则的原因不仅仅是因为破坏它是不好的, 因为似乎了解软件体系结构的人告诉你这是不好的。

使用时视图模型Architecture Components中的类(旨在具有其实例坚持比片段/活动生命周期长在适当的时候), 则引用View要求严重的内存泄漏.

至于为什么MVVM通常不允许这样的引用, 目标是假设地使View和ViewModel都更易于测试和编写。

其他人可能还指出, 它可以提高ViewModels的可重用性, 但这是正是这种模式在哪里分解了.

在查看代码之前, 请注意我个人不使用LiveData在我自己的生产代码中。这些天我更喜欢编写自己的发布者-订阅者模式, 但是我下面所说的适用于任何允许从ViewModel到View的PubSub / Observer Pattern链接的库。

本文随附了一个视频教程, 其中包含许多相同的想法:

ViewLogic + ViewModel还是View + ViewModelController?

当我在上一节中说"崩溃"时, 我并不是说该模式实际上已经崩溃。我的意思是, 它分解为(至少)两种不同的方法, 这些方法具有非常不同的外观, 好处和后果。

让我们考虑这两种方法, 以及你可能希望优先于另一种方法。

如何像Pro一样在Android上使用Model-View-ViewModel1

Boromir解释说MVVM不是魔术棒, 它使应用程序的表示逻辑消失了。

第一种方法:确定可重用的ViewModel的优先级

据我所知, 大多数实现MVVM的人都将促进ViewModel的可重用性作为一个目标, 以便可以将它们重新用于ñ不同视图的数量(多对一的比例)。

简单来说, 有两种方法可以实现这种可重用性:

  • 通过不引用特定的视图。希望这对你而言不是新闻。
  • By会心尽可能少的细节UI一般来说

第二点听起来可能含糊不清或违反直觉(它怎么能知道它未引用的内容?), 所以我认为现在该看一些代码了:

class NoteViewModel(val repo: NoteRepo): ViewModel(){
    //Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC
    val noteState: MutableLiveData<Note>()
    //...
    fun handleEvent(event: NoteEvent) {
        when (event) {
            is NoteEvent.OnStart -> getNote(event.noteId)
            //...
        }
    }
    private fun getNote(noteId: String){
        noteState.value = repo.getNote(noteId)
    }
}

尽管这是一个非常简化的示例, 但要点是, 此特定ViewModel公开公开的唯一内容(不是handleEvent函数)是一个简单的Note对象:

data class Note(val creationDate:String, val contents:String, val imageUrl: String, val creator: User?)

通过这种特殊方法, ViewModel不仅可以与特定的View完全分离, 而且可以与细节分离, 并且通过扩展, 表达逻辑任何特定视图。

如果我说的话仍然含糊不清, 我保证一旦我描述了另一种方法就将很清楚。

尽管我之前的标题是"ViewLogic + ViewModel…"并不意味着要被使用或重视, 我的意思是说, 通过具有高度分离和可重用的ViewModel, 我们现在依靠View本身来完成如何在屏幕上呈现/绑定此Note对象的工作。

我们中有些人不喜欢用Logic填充View类。

这是事情变得非常混乱并取决于项目的地方要求。我并不是说用以下逻辑填充View类:

private fun observeViewModel() {
    viewModel.notes.observe(
        viewLifecycleOwner, Observer { notes: List<Note> ->
            if (notes.isEmpty()) showEmptyState()
            else showNoteList(notes)
        }
    )
   //..
}

…是

总是

这是一件坏事, 但是与平台紧密耦合的类(例如Fragments)很难测试, 而其中包含逻辑的类是最重要的测试类!

一言以蔽之, 应用我认为是任何好的架构的黄金原则都是失败的:

关注点分离

.

我个人的看法是, 高度重视分离问题是值得的。但是, 毫无疑问的是, 对于那些意味着什么的最微不足道的线索的人们已经写了很多摇钱树的申请书。

无论如何, 接下来我们将讨论的方法有自己的副作用, 再次从视图中删除表示逻辑。

好吧, 无论如何大部分都是这样。

第二种方法:谦虚视图, Control-Freak ViewModel

有时, 无法对你的View进行细粒度的控制(这是优先考虑ViewModels的可重用性的结果), 实际上有点糟。

为了使我对狂热地应用以前的方法更加不感兴趣, 我发现我经常 不要 需要重用ViewModel.

具有讽刺意味的是, "太多抽象"是对MVVM的MVP的普遍批评。

话虽如此, 我们不能简单地将引用重新添加到ViewModel中以重新获得对View的这种细粒度控制。基本上这只是MVP +内存泄漏(假设你仍在使用AAC的ViewModel)。

然后, 另一种方法是构建你的ViewModel, 使其包含几乎所有的行为, 州和表达逻辑给定视图的当然, View仍必须绑定到ViewModel, 但是ViewModel中存在有关View的足够详细信息, 以使View的功能简化为一个衬里(少数例外)。

在Martin Fowler的命名约定中, 这称为被动视图/屏幕。此方法更通用的名称是谦虚的对象模式.

为了实现这一点, 你必须让ViewModel本质上为View中存在的每个控件或小部件拥有一个可观察字段(无论如何实现–数据绑定, Rx, LiveData等):

class UserViewModel(
    val repo: IUserRepository, ){

    //The actual data model is kept private to avoid unwanted tampering
    private val userState = MutableLiveData<User>()

    //Control Logic
    internal val authAttemptState = MutableLiveData<Unit>()
    internal val startAnimation = MutableLiveData<Unit>()

    //UI Binding
    internal val signInStatusText = MutableLiveData<String>()
    internal val authButtonText = MutableLiveData<String>()
    internal val satelliteDrawable = MutableLiveData<String>()

    private fun showErrorState() {
        signInStatusText.value = LOGIN_ERROR
        authButtonText.value = SIGN_IN
        satelliteDrawable.value = ANTENNA_EMPTY
    }
    //...
}

随后, View仍然需要将其自身连接到ViewModel, 但是这样做所需的功能变得很容易编写:

class LoginView : Fragment() {

    private lateinit var viewModel: UserViewModel
    //...
    
    //Create and bind to ViewModel
    override fun onStart() {
        super.onStart()
        viewModel = ViewModelProviders.of(
        //...   
        ).get(UserViewModel::class.java)

        //start background anim
        (root_fragment_login.background as AnimationDrawable).startWithFade()

        setUpClickListeners()
        observeViewModel()

        viewModel.handleEvent(LoginEvent.OnStart)
    }

    private fun setUpClickListeners() {
      //...
    }

    private fun observeViewModel() {
        viewModel.signInStatusText.observe(
            viewLifecycleOwner, Observer {
                //"it" is the value of the MutableLiveData object, which is inferred to be a String automatically
                lbl_login_status_display.text = it
            }
        )

        viewModel.authButtonText.observe(
            viewLifecycleOwner, Observer {
                btn_auth_attempt.text = it
            }
        )

        viewModel.startAnimation.observe(
            viewLifecycleOwner, Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName)
                )
                (imv_antenna_animation.drawable as AnimationDrawable).start()
            }
        )

        viewModel.authAttemptState.observe(
            viewLifecycleOwner, Observer { startSignInFlow() }
        )

        viewModel.satelliteDrawable.observe(
            viewLifecycleOwner, Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(it, "drawable", activity?.packageName)
                )
            }
        )
    }

你可以找到此示例的完整代码这里.

你可能已经注意到, 我们可能不会重复使用此ViewModel其他任何地方。而且, 我们的View变得足够谦虚(取决于你的标准和代码覆盖率的偏好), 并且非常易于编写。

有时, 你会遇到这样的情况, 即你必须在表达逻辑在Views和ViewModels之间, 它们并不严格遵循这两种方法。

我并不是在倡导一种方法, 而是要根据你的要求鼓励你在方法上保持灵活性。

根据首选项和要求选择架构

本文的目的是研究开发人员可以在Android平台上构建MVVM风格的GUI架构时采用的两种不同方法(有些可以移植到其他平台)。

实际上, 即使在这两种方法中, 我们也可以更详细地说明细微的差异。

  • View应该为它拥有的每个小部件/控件观察一个字段, 还是应该观察一个发布单个控件的字段模型每次重新渲染整个视图?
  • 也许我们可以通过将诸如Presenter或Controller之类的东西简单地添加到混合中而避免将ViewModels一对一地建立, 同时将Views保持为Humble Objects?

谈话很便宜, 我强烈建议你尝试学习这些内容在代码中这样你就无需依靠像我这样的人来告诉你该怎么做。

最终, 我认为构成一个优秀架构的两个因素归结为以下考虑因素:

首先, 尝试几种方法, 直到找到一种方法偏爱。最好通过实际构建每种样式的应用程序(可能很简单)并查看有什么效果来完成此操作感觉不错.

其次, 要理解, 除了偏好, 不同的风格将倾向于强调不同的利益, 以换取不同的赤字。最终, 你将能够基于对项目需求的理解来选择不错的选择, 而不是迷信.

了解有关软件体系结构的更多信息:

社会的

https://www.instagram.com/rkay301/

https://www.facebook.com/wiseassblog/

http://wiseassblog.com/

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: