【问题标题】:How to model parent-child relationship in Android MVVM VMs?如何在 Android MVVM 虚拟机中建模父子关系?
【发布时间】:2021-01-28 00:26:28
【问题描述】:

我正在开发一款 Android 钢琴“测验”应用程序 - 用户点击钢琴键,然后单击黄色的“检查”按钮提交答案以供评估,并查看在钢琴上绘制的正确答案。主要的QuizActivity 有这样的布局:

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

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

换句话说,QuizActivityViewModel 应该拥有/成为PianoViewViewModel 的父级,以促进通信和数据共享。

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

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

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

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

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

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

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

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

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

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

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

【问题讨论】:

    标签: java android mvvm android-viewmodel dagger-hilt


    【解决方案1】:

    我想你从上面的 Pavlo 那里得到了一个不错的答案,我会用其他词澄清他的意思。

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

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

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

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

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

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

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

    // One QuizActivityViewModel, Multiple Fragments:
    
    Activity -> PianoFragment (PianoView)| 
                                         | <-> PianoViewModel <-> KeyRepo
                PianoFragment (PianoView)|                       /
                -> QuizActivityViewModel <----------------------/
    
    

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

    或者

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

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

    流程将以sealed class 表示,其中将包含足够的信息供 QuizActivity + VM(可能执行实时验证)、到 PianoViewModel 更新状态和将新状态推送到 PianoFragment(这将更新其视图的状态)。

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

    这对你有意义吗?

    【讨论】:

    • 谢谢!这让它更清楚了一点,但我仍然没有得到几分。例如。为什么我需要将我的PianoViews 包裹在Fragments 中? AFAIK ViewModel 可以附加到 View,就像它可以附加到 FragmentActivity 一样。我认为它是一个不必要的包装。另外,如果我为PianoVMPianoView 选择一个1:1 的场景,只有一个KeyStateRepository,用@ActivityRetainedScope 来限制repo 范围是个好主意吗? (只要我不想在应用关闭后保持状态)
    • 很高兴听到它有帮助。让我试着在评论中给你我的意见(空间有限):“为什么使用 Fragment”:从技术上讲,你不需要 ,但 Fragment 是每个人都方便且不需要的(除了 Google)神器。将其视为不必要的包袱,很可能会让您的生活更轻松。视图本身没有(afaik)Activity 和 Fragments 具有的 viewModel 关系。我有 1 个 Activity,1 个 Fragment,持有 N 个视图。我不会在活动和片段之间共享 Viewmodel,这样可以避免“错误地”耦合它们。
    • 关于 ActivityRetainedScope,我会说是的,你可以这样做,假设就像你说的那样,一旦 PianoActivity(和所有 Fragment/Views/Etc.)完成你不会注意保持这种状态。我没有设计好的 Dagger/Hilt 组件和范围的经验(尤其是没有看到模块和依赖树)。如果您希望整个 Piano* 组件树在 Activity 被终止时“死亡”,那么是的,我会说 ActivityRetainedScope 是要走的路。尝试一下,然后尝试LeakCanary 来检查您的参考资料。
    【解决方案2】:

    编辑

    在多架构活动中,如果您不想让PianoViewsViewModels 和您的ActivityViewModel 了解它们 - 不要将Dagger 与它们一起使用,而是在@987654330 中创建PianoViewModels @ 并在创建阶段为它们分配一些回调 - 这样您就可以访问它们,并且能够从 ActivityViewModel 内部监听它们的事件并影响它们的行为以及保存它们的状态。这并不少见,在某些情况下甚至是正确的方法。 Dagger - 只是一种工具,不打算在任何地方使用,但只有在需要时才使用。不需要创建PianoViewModels - 您可以将所有需要的东西注入ActivityViewModel 并将所有需要的元素传递给PianoViewModels 构造函数。

    如果你不想的话,你也不需要把你的视图包装成片段。

    编辑结束

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

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

    如果您需要共享视图模型,并且您将使用具有多个片段的单一活动原则,共享视图模型将起作用。因为Jetpack Navigation 支持开箱即用的共享视图模型。在共享视图模型的情况下 - 每个片段都有自己的视图模型以及用于通信的共享视图模型。每个navigation graph 可以有一个单独的共享视图模型,仅用于它包含的片段。

    另外关于KeyStateRepository - 你只需要其中一个(或 Dagger @Scoped 多个副本 - 但我不推荐它)。唯一的变化应该是 - 为每个单独的 PianoView 添加一个额外的密钥 - 以在 KeyStateRepository 中区分它们。为了轻松实现这一点,您可能正在使用Room 或其他一些文件/内存/数据库缓存机制。

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

    【讨论】:

    • 谢谢。我的应用程序使用多活动架构。除了使用NavGraph 之外,我没有看到使用单一活动架构的任何直接好处。即使是 Android 开发人员say 也无需重写现有应用程序即可迁移到 SAA。 MVVM 早于 Jetpack Navigation AFAIK。我的活动直接托管其他视图(即按钮),这就是为什么我认为它应该有一个ViewModel。因此,您提出的解决方案是重写存储库以通过 ID 密钥处理多个 PianoViews,而无需任何 VM 间通信?
    • 关于架构 - 可以理解。关于存储库 - 是的,我认为您应该只有一个具有唯一 ID 用于内部多个单独的 PianoView 状态。如果您不希望 PianoViews 拥有 ViewModels 和您的 ActivityViewModel 来了解它们 - 不要对它们使用 Dagger 注入,而是在 ActivityViewModel 中创建 PianoView ViewModels 并在创建阶段为它们分配一些回调 - 这样你就可以访问并能够从 ActivityViewModel 内部监听他们的事件并影响他们的行为以及保存他们的状态。
    • 非常感谢。我觉得我应该解释一下奖励:我读了你和@Martin 的回复几次,我觉得他的回答更加完整和详细。所以,虽然你的回答在技术上是正确的并且排在第一位,但我最终还是决定接受他的回答。我希望我可以在你们两个之间分配赏金,但 SO 不允许这样做。很抱歉,至少你有我的赞成票。
    • 我不反对你把名誉给别人,因为它是有价值的。谢谢。
    【解决方案3】:

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

    如何获得ActivityViewModel 是另一个问题。查看片段的by activityViewModels() 实现,您可能可以通过by viewModels() 传递活动的viewModelStore 来做同样的事情

    【讨论】:

    • 嗯...实际上,我需要依赖的倒置情况-ActivityViewModel 应该依赖于PianoViewModel,而不是相反。我不认为我可以从 viewModel 中获得 Activity - VM 不应该对 View 有任何了解,甚至在官方 Android Docs 中也有说明 requireActivity() 仅在 Fragment 中可用。另外,我使用的是 Java,而不是 Kotlin,尽管这并不重要。
    • 可以,但是你可以在片段中发送事件或访问虚拟机的状态,它可以与你的接口/抽象进行通信
    • 我不确定我是否遵循。您是否建议我应该用Fragment 包装PianoView?然后什么?我仍然需要打电话给例如PianoViewModel#getSelectedKeys 来自ActivityViewModel,我不知道该怎么做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-06
    • 2019-08-04
    相关资源
    最近更新 更多