本文译自「Handling UI Actions the Right Way in Kotlin ViewModels」,原文链接https://proandroiddev.com/handling-ui-actions-the-right-way-in-kotlin-viewmodels-119a06bb43ef,由Vaibhav Jaiswal发布于2025年4月16日。
缘起
作为Android开发者,我们经常会遇到需要在多个ViewModel中实现相同或者非常类似UI功能的情况。
例如,我们有多个页面,它们具有类似的功能,例如显示帖子、撰写评论或处理用户交互。
在每个ViewModel中分别处理这些UI交互很快就会变得混乱,导致大量的代码重复。随着应用规模的扩大和页面数量的增加,这个问题会变得更加棘手,导致代码库难以维护,并带来可扩展性问题。
对于这种代码重复问题,我们Android开发者常用的几种常见解决方案包括:
- 额外的辅助类方法
- “继承”或“使用委托的组合”方法
在以下章节中,我们将详细探讨这些解决方案,了解每种方法如何解决代码重复问题,并重点介绍每种方法的局限性。然后,我们将深入探讨我的解决方案,它基于这些想法,并解决了它们的局限性,从而实现了更高效的UI交互管理。
这个解决方案我已经在 Medial 的应用(链接 https://medial.app/%EF%BC%89%E4%BB%A3%E7%A0%81%E5%BA%93%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%BF%87%E4%BA%86%E2%80%94%E2%80%94%E5%B0%86 UI交互处理代码变成了近乎即插即用的体验。
我的解决方案根本不建议使用 BaseViewModel,也不基于 BaseViewModel。BaseViewModel 只是一个例子,用来说明我们可以使用从任何其他类/接口继承的功能,这些类/接口可以是 BaseViewModel、ViewModel、Decompose 的 ComponentContext 或其他任何东西。
⚡️ TL;DR: 处理ViewModel中的共享UI交互
当在多个页面上显示相同的UI组件时,在每个ViewModel中处理它们的交互会导致重复和逻辑混乱。我们探索了三种方法来解决这个问题:
- 辅助类方法
- ➖ 简单,但无法覆写任何行为
- ➖ 并非ViewModel的直接功能
- 通过 Kotlin 委托进行组合
- ✅ 更好的设计,支持覆写行为。
- ➖ 无法访问viewModelScope或其他ViewModel功能。
- ➖ 无法从任何其他继承类访问任何内容。
- 💡 我的解决方案 — 使用带有默认函数的接口
- ✅ 简洁、可复用,支持覆写(Override)行为。
- ✅ 完全访问ViewModel功能或任何其他继承类的功能。
- ✅ 只需从接口实现即可轻松插入任何 ViewModel
本博客中的所有解决方案均在此示例项目(链接 https://github.com/Vaibhav2002/Ui-Intreraction-Handler-Sample%EF%BC%89%E4%B8%AD%E5%AE%9E%E7%8E%B0%EF%BC%9A
🏗️ 设置
假设我们有一个后置UI 元素,它有一些UI 交互,并像 MVI 建议的那样,在一个密封的界面中呈现。
1 2 3 4 5 |
|
我们还有一个 BaseViewModel 类,用于保存每个ViewModel所需的通用功能
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
额外的辅助类方法
在这种方法中,我们创建一个单独的辅助类来封装UI交互的处理。我们将这个类作为ViewModel的一个属性。
首先,我们定义一个辅助类 PostActionHandler 来处理交互。
1 2 3 4 5 6 7 8 |
|
现在,在 PostScreenViewModel 中我们创建一个 PostActionHandler 实例
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
虽然这种方法确实解决了跨多个ViewModel重复代码的问题,但它也存在一些主要缺点,这些缺点包括:
- 🚫 自定义功能有限:由于 PostActionHandler 是一个封装类,我们无法覆写任何行为。
- 🚫 通过属性访问:我们不是将功能添加到ViewModel本身,而是将其作为ViewModel的一个属性添加。
无法覆写(Override)行为是这种方法不建议用于UI交互处理的主要原因。
🧬 使用 Kotlin 委托进行组合(优雅但受限的解决方案)
这是互联网上解决这个问题的标准方法,也是最受推荐的方法。该解决方案基于继承,但不是继承自某个类,这样就无法再扩展任何类。我们利用 Kotlin 委托将实现委托给另一个类,这样就无需扩展某个类,也无需再终止继承。
以下是我们实现该解决方案的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
这就是我们的使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这种方法解决了额外辅助类方法中存在的所有问题。
- ✅ 覆写行为:通过使用接口和委托,我们可以轻松地覆写任何行为。
- ✅ 更简洁的设计:功能直接成为ViewModel的一部分,允许我们像调用原生ViewModel方法一样调用这些函数。
然而,这种方法有一个主要缺点,使其在处理UI交互和UI逻辑方面不够完善。
访问从其他类继承的功能
此模式的一个主要挑战是我们无法访问从其他类继承的功能,因为我们无法传递引用。
让我们看看这个限制带来了哪些挑战:
- 无法访问ViewModel功能:我们无法从实现类访问任何ViewModel功能。这是因为我们无法在实现类中传递当前类的引用。
- 无法使用 viewModelScope:此限制带来的一个主要警告是,我们无法使用 viewModelScope,因此无法直接启动协程。我们必须在 viewModel 中创建包装函数,这会破坏可重用性,因为现在可组合函数会调用我们的ViewModel函数。
当我们尝试将其作为构造函数参数传递时,Android Studio 会抛出一个错误:
🧩 我的解决方案:使用接口默认函数进行组合
在我自己实现使用委托的 Composition 方案时,我遇到了一个难题:如何传递当前ViewModel的引用来访问viewModelScope 和我的 BaseViewModel 功能。后来我想起了 Kotlin 接口中的默认函数,于是尝试了这个方案,完美地解决了这个问题。
通过这个方案,我可以:
- 💡 支持功能重写
- 🔗 完全访问我的BaseViewModel和ViewModel 的功能,例如viewModelScope 等,或任何其他类的功能。
- ❌ 无需单独的实现类
在我们 Medial 的应用(链接 https://medial.app/%EF%BC%89%E7%94%9F%E4%BA%A7%E4%BB%A3%E7%A0%81%E4%B8%AD%EF%BC%8C%E6%88%91%E9%9B%86%E6%88%90%E4%BA%86%E8%BF%99%E4%B8%AA%E6%96%B9%E6%A1%88%EF%BC%8C%E4%BB%A5%E7%AE%80%E5%8C%96ViewModel%E5%A4%84%E7%90%86UI%E4%BA%A4%E4%BA%92%E7%9A%84%E6%96%B9%E5%BC%8F%E3%80%82%E7%BB%93%E6%9E%9C%E5%A6%82%E4%BD%95%EF%BC%9F%E6%AF%8F%E4%B8%AA%E6%96%B0%E9%A1%B5%E9%9D%A2%E5%8F%AA%E9%9C%80%E5%AE%9E%E7%8E%B0%E8%BF%99%E4%B8%AA%E6%8E%A5%E5%8F%A3%EF%BC%8C%E7%84%B6%E5%90%8E%EF%BC%8C%E5%AE%83%E5%B0%B1%E5%B9%B2%E5%87%80%E5%88%A9%E8%90%BD%E5%9C%B0%E8%8E%B7%E5%BE%97%E4%BA%86%E5%8A%9F%E8%83%BD%E3%80%82
让我们来探索一下这个方案是如何解决我们的问题的。
设计 ActionHandler 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
让我们的ViewModel实现这个接口
1 2 3 4 5 6 7 8 9 10 |
|
这是它在 Composable 中的使用方式
1 2 3 4 5 6 7 8 9 10 11 |
|
所以,正如你所见,我们只需要定义 viewModel 变量,就大功告成了,现在我们可以使用ViewModel和 BaseViewModel 提供的所有功能了。
请注意,我们是如何解决使用委托进行组合的主要缺点的:
- 通过将 viewModel 引用保留为接口属性,我们可以轻松地在ViewModel中定义它。这使我们能够直接访问 BaseViewModel 或任何其他ViewModel提供的所有功能。
- 我们简化了 viewModelScope 的使用。现在,启动协程、收集流程以及执行其他任务都变得非常简单,无需任何复杂的变通方法或 hack。这就是此解决方案带来的优雅和简洁之处。
这种方法只有一个小缺点,那就是
- 我们必须在ViewModel中定义所有接口属性,这在大多数情况下是可以接受的,因为我们主要会引用 Repository 或 Domain Layer 类,以及一些可变状态(如果有的话)。
这个问题比我们在这个解决方案中克服的缺点要小得多,这使得它成为处理任何共享UI交互或任何共享UI业务逻辑的最佳解决方案。
嗯,这一部分比前面几部分简单得多。这里没有复杂的问题需要深入研究——只是一个简单的解决方案,它确实做到了它应该做的事情,并解决了我们所有的问题。
🥷🏻 让我们在实际用例中看看这个解决方案的实际应用。
假设我们有一个主页面,它以列表形式显示各种UI元素,例如包含帖子和新闻等项目的动态。每个元素都包含一组用户交互,例如点赞帖子、分享新闻文章或导航到详情视图。我们的解决方案可以帮助我们高效地处理跨不同ViewModel的这些交互,确保以可扩展的方式处理通用功能。
为每个UI组件设计UI交互
然后,我们使用 Kotlin 的 Sealed 接口,像在典型的 MVI 架构中一样设计UI交互:
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 |
|
创建我们的动作处理程序接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
如上所示,我们为每个数据模型创建了一个 Action Handler 接口。
我们在这里所做的就是遵循上一节所示的接口设计。
- 我们有两个属性,分别是ViewModel和Repository 类。
- 我们分别创建了处理每个 Action 的函数。当然,这是可选的。我保留这种方式是为了方便覆写任何特定的行为,并且我们为每个 Action 类型分别调用相应的 Action 处理函数。
创建 ViewModel
让我们构建一个实现这两个接口的 ViewModel,以继承它们的功能。
1 2 3 4 5 6 7 8 9 10 11 |
|
然后,我们在ViewModel中定义必要的属性( viewModel 、 postRepo 和 newsRepo ),这些属性会被两个接口使用。
这种设计消除了冗余,并保持了代码的简洁性,因为我们只需设置一次属性,只要属性名称相同,两个接口就可以无缝地与其交互。 这种方法保持了ViewModel的中心地位,同时又可以轻松处理不同组件的UI交互和业务逻辑。
🪝连接一切:将操作连接到 UI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
在我们的UI组件中,我们可以直接将这些函数用作ViewModel本身的函数,这样就完成了在 Screen 中添加交互处理的功能。
- 现在,假设我们有另一个只显示新闻的页面(Screen)。我们需要做的就是在ViewModel中实现 NewsActionHandler 接口——就这样,一切就绪。
- 我们可以无缝地从 BaseViewModel 或ViewModel调用任何函数,从而能够在一个单一的、集中的位置实现完整的端到端功能。如果我们在多个位置显示相同的数据模型,这将非常有利。
📦 整合所有功能
在这篇博文中,我们探讨了在扩展Android UI(基本上是任何 UI)时最容易被忽视的问题之一,即跨ViewModel的重复UI交互逻辑。我们探索了三种不同的方法来解决这个问题:
- 辅助类 - 一种快速解决方案,但存在严格的限制。
- 使用委托进行组合——一种更现代的继承驱动解决方案, 但它难以从外部访问功能。
- 使用默认函数的接口(我的解决方案)——一种实用、优雅且灵活的方法,克服了前两种方法的所有限制。
通过利用 Kotlin 的接口默认方法并将ViewModel作为接口内部的属性,我们解锁了完整的可扩展性,并能够从其他继承的类/接口访问扩展功能。
现在,让我们以表格的形式比较一下每个解决方案:
好处 | 通过辅助类 | 使用委托进行组合 | 带有默认函数的接口 (✅ 最好) |
---|---|---|---|
🔁 可重用性 | ✅ Yes | ✅ Yes | ✅ Yes |
🧠 覆写行为 | ❌ 不可能 | ✅ 可以,由于继承 | ✅ 可以,由于继承 |
🔗 访问从其他来源继承的功能 | ❌ 不可以,因为它是一个封装类 | ❌ 不可以,因为我们无法传递当前引用 | ✅ 可以,因为我们可以将继承类保留为接口属性 |
🏗️ 跨页面可扩展性 | ⚠️ 差——当逻辑还涉及视图模型特定功能时会变得困难 | ✅ 更好——可重复使用的逻辑 | ✅ 最佳——跨页面即插即用 |
✅ 最佳用例 | 对于非UI交互封装功能 | 业务逻辑重用,非UI上下文 | 多个ViewModel中需要的UI交互 |
你可以在专用的 GitHub 代码库(链接:https://github.com/Vaibhav2002/Ui-Intreraction-Handler-Sample%EF%BC%89%E4%B8%AD%E6%8E%A2%E7%B4%A2%E6%9C%AC%E5%8D%9A%E5%AE%A2%E4%B8%AD%E6%8F%90%E5%88%B0%E7%9A%84%E6%89%80%E6%9C%89%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E3%80%82%E6%AC%A2%E8%BF%8E%E9%9A%8F%E6%84%8Fclone%E3%80%81fork%E6%88%96%E8%AF%95%E7%94%A8%E3%80%82
✌️ 告别
总而言之,希望这篇博客能帮助你解锁一些强大的技巧,提升你的Kotlin 和UI处理知识。有了Kotlin 的界面默认函数,你的构建将更加简洁、快速,并兼顾长期可扩展性。
如果你喜欢这篇文章并想随时了解最新动态:
- 查看我的 LinkedIn、GitHub 和 X 个人资料,了解我正在做的事情。
- 想要更多 Kotlin 的魔法?阅读我之前的博客,了解另一种可以让你的代码库更精简、更智能的技术。
感谢你的阅读,希望你喜欢!🚀