为什么我不在 Android ViewModel 中直接处理异常?

702 阅读4分钟

ViewModel 应该不要处理异常? 更准确地说:ViewModel 不应该处理异常,它只需要“消费结果”。

本文结合实际项目中的常见做法,说明一种更清晰的职责划分: UseCase / Repository 负责处理异常,ViewModel 只关注数据与 UI 状态。

⚠️ 说明:本文中的异常模型是刻意简化的。 重点不在“异常如何分类”,而在于: 只要异常已经在下层被处理并转换成结果,ViewModel 就不应该再关心异常本身。


1. 背景:异常到底该谁来处理?

一个典型的 clean 分层大致如下:

  • UI 层(Activity / Fragment / Compose)
  • ViewModel
  • Domain 层(UseCase)
  • Data 层(Repository / DataSource / 网络、数据库等)

常见的疑问是:

ViewModel 拿到 UseCase 或 Repository 的数据,这些底层已经做了异常处理,那 ViewModel 还要再 try/catch 吗?

我的结论是:

如果 UseCase / Repository 已经把异常转换成稳定的数据结果,ViewModel 完全可以只关心“结果”,而不直接处理异常对象。


2. 分层职责:谁应该干什么?

2.1 Data / Domain 层(UseCase / Repository)

核心职责:

  • 捕获所有底层异常(网络、IO、数据库、第三方 SDK 等)
  • 在内部记录日志、上报监控、决定是否重试
  • 将异常统一转换为一个稳定、简单、可被上层消费的结果

为了突出 ViewModel 的职责边界,这里使用一个极简的结果模型

一个简单的 Repository 实现示例如下:

image.png 这里已经完成了所有异常相关的工作:

  • 捕获异常
  • 屏蔽底层实现细节(网络库、异常类型)
  • 向上层只暴露一个“成功 / 失败”的结果

2.2 ViewModel 层

核心职责:

  • 调用 UseCase / Repository
  • 根据返回的结果更新 UI 状态(loading / content / error)
  • 只处理结果分支,而不是异常本身

示例代码如下:

image.png

image.png

可以看到:

  • ViewModel 完全没有 try/catch
  • 不知道发生了什么异常
  • 只知道“成功”或“失败”
  • 只关心当前应该呈现怎样的 UI 状态

3. 为什么不建议在 ViewModel 处理异常?

3.1 破坏分层,增加耦合

如果在 ViewModel 中写这种代码:

image.png

那么:

  • ViewModel 将直接依赖底层实现
  • 网络库、数据源一旦更换,ViewModel 就需要修改
  • 异常处理逻辑分散在多个 ViewModel 中,难以复用

而这些本应是 Repository / UseCase 的职责。

3.2 错误处理难以统一

当多个 ViewModel 各自处理异常时,往往会出现:

  • 相同错误在不同页面展示方式不一致
  • 重复的异常处理逻辑到处复制
  • 想统一策略(例如:失败统一提示)成本极高

将异常处理集中在 UseCase / Repository,可以保证:

  • 策略统一
  • 行为可控
  • 上层逻辑极度简单

4. 建议的整体模式(总结)

可以用一句话概括:

UseCase / Repository 负责“把异常变成结果”; ViewModel 负责“把结果变成 UI 状态”。

UseCase / Repository:

  • 捕获所有外部系统异常

  • 处理日志、监控、重试等技术细节

  • 将结果统一转换为:

    • Success(data)
    • Error

ViewModel:

  • 不接触任何异常对象
  • 不做异常分类
  • 只根据结果更新 UI

5. 结语

只要异常已经在 UseCase / Repository 层被处理并转换成结果:

  • ViewModel 就不应该再关心异常本身
  • 它的世界里只剩下:成功或失败

这样的设计带来的好处是:

  • 分层清晰,职责单一
  • ViewModel 更容易测试
  • 代码更稳定、更容易维护

如果你现在的项目中,ViewModel 里还有大量 try/catch,可以从一两个接口开始,先把异常下移到 UseCase / Repository,逐步过渡到ViewModel 只消费结果的模式。

示例:🔗 github.com/yourname/Cl…