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

Lep*_*aun 10 java android mvvm android-viewmodel dagger-hilt

我正在开发一个 Android 钢琴“测验”应用程序 - 用户点击钢琴键,然后单击黄色的“检查”按钮提交答案进行评估,并查看在钢琴上绘制的正确答案。主要QuizActivity有这样的布局: 在此处输入图片说明

屏幕的上部包含几个控件(文本、提交按钮等)。屏幕的下部由一个自定义PianoView组件占据,用于处理钢琴键盘的绘制。

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

换言之,所述QuizActivityViewModel应该拥有/是的母体PianoViewViewModel以促进通信和数据共享。

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

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

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

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

Android 开发文档建议使用shared ViewModel来促进两个 Fragment / Views 之间的数据共享。但是,这不适合我的用例。的PianoView(或它的视图模型)应该是其状态的具有唯一所有者Repository作用域到其ViewModel。否则,PianoView组件将无法重用。考虑例如 another Activity,我希望有两个独立的PianoView实例可见:

在此处输入图片说明

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

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

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

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

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

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

Mar*_*ini 2

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

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

  2. PianoView 应包含在 Fragment 中,该 Fragment 应该是您的“单元”。为什么?因为您希望 ViewModel 处理传入/传出视图的状态和事件。一个Fragment是为此提供的 Android 工件。将其视为您需要的一件烦人的行李。Android 开发人员过去常常将这些东西称为“策略委托”,因为您将一些没有“框架”(即 Android 框架)就无法完成的事情委托给这些(片段/活动)。

  3. 考虑到这一点,您的ActivityviewModel/State 是独立处理的。这个 viewModel 处理什么状态/事件?不在PianoFragment/View 中的东西。例如,如果您想处理后退导航或顶部的“记录”按钮,这就是活动的域。“PianoView/Fragment”内部发生的事情不是此活动的问题。

  4. 现在,包含实际 PianoView 的片段可以设计为包含“多个”或仅包含一个。如果您选择多个 PianoContainerFragment,则 PianoContainerFragment 将使用一个 ViewModel 来设计,该 ViewModel 旨在处理多个 PianoView(因此每个视图都有一个“名称/键”),并且 KeyStateRepo 将能够处理“CRUD”操作您抛出的任何钢琴视图。ViewModel 将位于两者之间,为不同的“订阅”视图分派事件。

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

  6. 你并不真正需要一个共享的 viewModel 我认为,事实上,直到真正需要时我才会这样做,你将事物分开得越多,“违反”其中一种奇特模式的机会就越少……但是如果你选择使用 PianoViewModel 作为所有视图之间的共享,这是完全可以接受的,您必须包含 Piano“名称”来区分谁的事件是为谁服务的。

换句话说(使用 ONE PianoViewModel 实现 ASCII 简单性显示),

// One QuizActivityViewModel, Multiple Fragments:

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

Run Code Online (Sandbox Code Playgroud)

这里 QuizActivity 创建了 N 个片段(可能在一个列表中?)。这些片段在内部初始化它们的pianoView并连接到PianoViewModel(可以像上图一样共享)或者每个片段都可以拥有自己的。他们都与同一个 Repo 对话。存储库是您的“关于每个“钢琴”的唯一事实来源。按了哪些键,以及您能想到的任何其他内容(包括使其唯一的名称/键)。当 QuizActivity 需要评估这些状态时,它会(通过它自己的viewModel)询问 NN 钢琴的状态。

或者

// 1 Act. 1 Frag. N Views.
Activity -> PianoFragment (PianoView)| 
                          (PianoView)| <-> PianoViewModel <-> KeyRepo
         -> QuizActivityViewModel  <---------------------------/
Run Code Online (Sandbox Code Playgroud)

有了这些,QuizActivity(它也首先创建了钢琴)也知道将要显示的钢琴的琴键。它可以与其 viewModel 对话,而 viewModel 又与同一个 KeysRepo 对话(你只有其中之一,这很好)。因此它仍然可以处理“导航”按钮,并且可以(通过其QuizActVM)询问琴键的当前状态(对于所有涉及的钢琴)。当 PianoView 中触发 Piano 键事件时,PianoViewModel 将接收该事件(触摸了哪个键,在哪个钢琴中);KeyStateRepo 将记录这一点,并且可能会flow {}使用来自钢琴的事件更新...

Flow 将用 a 表示,sealed class其中包含 QuizActivity + VM(可能执行实时验证)PianoViewModel 的足够信息,以更新状态并将新状态推送到 PianoFragment(这将更新状态)其观点)。

这对于任何一种方法都是通用的。我希望这能澄清顺序。

你能理解这个吗?