如何在 Android MVVM 虚拟机中建立父子关系模型?

How to model parent-child relationship in Android MVVM VMs?

我正在开发一个 Android 钢琴“测验”应用程序 - 用户轻敲钢琴键,然后单击黄色的“检查”按钮提交答案进行评估,并在钢琴。主要 QuizActivity 具有以下布局:

屏幕的上部有几个控件(文本、提交按钮等)。 屏幕的下半部分被一个自定义 PianoView 组件占据,它处理钢琴键盘的绘制。

根据 MVVM 原则,PianoView 应该有自己的 PianoViewModel,用于存储其状态(即当前按下的键、突出显示的键、等...)在 KeysStateRepository 中。 封闭的 QuizActivity 还应该有一个 QuizActivityViewModel,用于处理各种控件(提交答案、跳过问题...)。 QuizActivityViewModel 需要能够从 PianoView(或者更确切地说是从它的 KeysStateRepository)查询选定的键,将它们提交到 域层 进行评估,然后将结果发送回 PianoView 进行可视化。

换句话说,QuizActivityViewModel 应该 own/be 是 PianoViewViewModel 的父级,以促进通信和数据共享.

我如何建模这种父子关系以在 ViewModel 之间进行通信?

AFAIK a ViewModel 不能依赖另一个 ViewModel(我会通过什么作为 ViewModelStoreOwner 来获得另一个 Viewmodel 中的 ViewModel?) .我认为至少 Dagger-Hilt 是不可能实现的。

想到了解决此问题的三个解决方案,但都无法使用:

1 - 在视图之间共享数据的官方方式

Android 开发文档 recommend 使用 shared ViewModel 来促进两个片段/视图之间的数据共享。但是,这不适合我的用例。 PianoView(或其 ViewModel)应该是其状态的唯一所有者,Repository 范围限于其 ViewModel。否则,PianoView 组件将不可重用。例如考虑另一个 Activity,我希望有两个独立的 PianoView 实例可见:

重用测验 activity 中的共享 ViewModel 显然是错误的,因为它包含不相关的方法和逻辑(即提交测验答案)并且不适合双键盘场景。

2 - 应用程序范围的存储库

Reddit 上解决了一个类似的问题,提出了使用存储库共享实例的解决方案。但是,使用 @Singleton KeyStateRepository 将再次阻止两个独立的键盘显示不同的数据。

3(EDIT) - 事件总线复制了 2 个重复的存储库

理论上我可以创建 2 个独立的 ViewModel 和 2 个 KeyStateRepository 实例。 ViewModels 将订阅事件总线。每次 ViewModel 在其存储库上调用可变操作时,它也会触发一个事件,并且该操作将通过订阅同一事件总线的另一个 ViewModel 进行复制。

但是,这感觉像是一个脆弱而复杂的黑客攻击。我想要一个简单的 MVVM 兼容解决方案。我无法相信两个 UI 组件的简单父子关系在 MVVM 中是无法实现的。

如果您不想将 PianoViewModel 绑定到您的 ActivityViewModel,我会执行以下操作,我会创建一个 interface,[=11] =] 实现,并且 PianoVM 可以对该接口有一个可为空的引用。这样,PianoViewModel 工作既不需要实施,也不需要组件的存在。

如何获得 ActivityViewModel 是另一个问题。查看片段的 by activityViewModels() 实现,您可能可以使用 by viewModels() 传递 activity 的 viewModelStore 而不是

编辑

在多架构中 activity 如果您不想 PianoViewsViewModels 和您的 ActivityViewModel 了解它们 - 不要使用 Dagger 与他们一起注入,但在 ActivityViewModel 中创建 PianoViewModels 并在创建阶段为他们分配一些回调 - 因此您将可以访问他们并能够收听他们的事件和影响他们的行为以及从 ActivityViewModel 内部保存他们的状态。这并不少见,在某些情况下甚至是正确的方法。 Dagger - 只是一种工具,并非旨在随处使用,但仅在需要时使用。不需要创建 PianoViewModels - 您可以将所有需要的东西注入 ActivityViewModel 并将所有需要的元素传递给 PianoViewModels 构造函数。

此外,如果您不想,也不需要将视图包装到片段中。

编辑结束

您基于有缺陷的架构方法做出了错误的假设。

我很好奇你为什么需要 ActivityViewModel。视图模型应该只存在于具有某些视图的元素中。当前的 android 开发建议 Activity 不应具有视图表示,而应仅作为其他视图的容器 (Single activity principle)。根据您的体系结构,Activity 可能会处理显示加载状态(进度条)和一些错误,但它不应该包含其他视图正在处理的任何内容。因此 PianoView 应该是一个 PianoFragment,它有自己的 ViewModel,它通过域层上的交互器处理对其在数据层上的存储库的访问。

共享视图模型可以在您需要的情况下使用,并且您将对多个片段使用单一 activity 原则。因为 Jetpack Navigation 开箱即用地支持共享视图模型。在共享视图模型的情况下——每个片段都有自己的视图模型以及用于通信的共享视图模型。每个 navigation graph 只能为其包含的片段有一个单独的共享视图模型。

关于 KeyStateRepository - 你只需要其中一个(或 Dagger @Scoped 多份 - 但我不推荐它)。唯一的变化应该是 - 为每个单独的 PianoView 添加一个额外的键 - 以在 KeyStateRepository 中区分它们。为了轻松实现这一点,您可以使用 Room 或其他一些 file/memory/database 缓存机制。

因此,您的应用程序最初的问题不是 ActivityViewModelPianoViewModel 的反向依赖,而是应用程序及其内部交互的有缺陷的架构。如果您想继续使用当前的架构 - 您的问题没有简单的答案,而且几乎每个选择的解决方案都'clean' 不足以证明其使用的合理性。

我认为你从 Pavlo 那里得到了一个不错的答案,我只是用其他词来澄清他的意思。

  1. KeyStateRepository是存储琴键状态的。没有什么能阻止您同时支持 N 台钢琴,这将解决屏幕上有 NNN 台钢琴,每台钢琴都按下不同键的情况。

  2. PianoView 应该包含在 Fragment 中,那应该是您的“单元”。为什么?因为你想要一个 ViewModel 来处理来自视图的状态和事件 to/from。 Fragment 是为这方面提供的 Android 神器。将其视为您需要的一件烦人的行李。 Android 开发人员过去称这些东西为“政策代表”,因为您将一些事情委托给这些(Fragment/Activity),如果没有“框架”(即 Android 框架),您将无法完成这些事情.

  3. 考虑到这一点,您有一个 Activity,其 viewModel/State 是独立处理的。这个 viewModel 处理什么 State/Events?在 PianoFragment/View(s) 中 不是 的东西。例如。如果您想处理后退导航或顶部的“记录”按钮,这是 activity 的域。 “PianoView/Fragment”内部发生的事情不是这个 activity 的问题。

  4. 现在可以将包含实际 PianoView 的 Fragment 设计为包含“多个”或仅包含一个。如果你选择不止一个,那么 PianoContainerFragment 将设计一个 ViewModel 来处理多个 PianoView(因此每个视图都有一个“name/key”)并且 KeyStateRepo 将能够处理“CRUD " 任何你投入的 Piano View 的操作。 ViewModel 将介于两者之间,为不同的“订阅”视图调度事件。

  5. 如果您选择“一个片段包含一个钢琴视图”,那么它是一个类似的架构,但现在在一个“activity”中处理多个“片段” Activity(及其视图模型)的责任。但请记住,PianoViews(通过共享或不共享的 Fragment)与可以在钢琴视图之间共享的 ViewModel 对话,与公共 KeyState Repo 对话。 activity 协调视图和其他 Android 事物(导航等),但视图独立运行,甚至相互独立。

  6. 我认为你真的不需要共享 viewModel,事实上,除非真正需要,否则我不会这样做,你分离的东西越多,“违反”一个的机会就越少花哨的模式...但是如果您选择使用 PianoViewModel 作为所有视图之间的共享,那是完全可以接受的,您将必须包含钢琴“名称”以区分谁的事件是为谁准备的。

换句话说(显示一个 PianoViewModel 以实现 ASCII 简单性),

// One QuizActivityViewModel, Multiple Fragments:

Activity -> PianoFragment (PianoView)| 
                                     | <-> PianoViewModel <-> KeyRepo
            PianoFragment (PianoView)|                       /
            -> QuizActivityViewModel <----------------------/

这里的测验Activity 创建了 N 个片段(也许在一个列表中?)。这些片段在内部初始化它们的 pianoView 并连接到 PianoViewModel(可以像上图中那样共享)或者每个片段都可以有自己的。他们都与同一个 Repo 交谈。回购协议是你的“关于每个“钢琴”的唯一真相来源。按下了什么键,以及你能想到的任何其他东西(包括 name/key 以使其独一无二)。 当 QuizActivity 需要评估这些状态时,它将询问(通过其 own viewModel)NN 钢琴的状态。

// 1 Act. 1 Frag. N Views.
Activity -> PianoFragment (PianoView)| 
                          (PianoView)| <-> PianoViewModel <-> KeyRepo
         -> QuizActivityViewModel  <---------------------------/

有了这些,QuizActivity(它也开始创建了钢琴)也知道 will/are 显示的钢琴的键。它可以与其 viewModel 对话,而 viewModel 与同一个 KeysRepo 对话(你只有其中之一,这很好)。所以它仍然可以处理“导航”按钮,它可以询问(通过它的QuizActVM)键的当前状态是什么(对于所有相关的钢琴)。当在 PianoView 中触发钢琴键事件时,PianoViewModel 将接收该事件(触摸了什么键,在什么钢琴上); KeyStateRepo 将记录下来,并可能用来自钢琴的事件更新 flow {}...

Flow 将以 sealed class 表示,其中包含足够的信息用于测验 Activity + VM(可能执行 real-time 验证), 到 PianoViewModel 以更新状态并将新状态推送到 PianoFragment(这将更新其视图的状态)。

这两种方法都是通用的。我希望这能澄清顺序。

你觉得这有意义吗?