Flutter Clean 架构中的 Riverpod 在哪里
本篇文章与你深入探讨在 Flutter 中如何运用 Clean Architecture,并结合 Riverpod、Go_Router 和 Retrofit 这套现代化工具栈,打造一个高内聚、低耦合、可扩展、易维护的顶级应用架构。
我们将从“道”的层面(高屋建瓴的架构哲学)入手,再深入到“术”的层面(具体分层、组件和痛点解决方案),并最终通过一个实例来将理论落地。
第一部分:高屋建瓴的架构统领 (The "Why" and "How" at 30,000 Feet)
在引入任何具体技术之前,我们必须先确立架构的核心指导思想。对于 Clean Architecture + Riverpod + Go_Router + Retrofit 这套组合拳,其核心思想是:
“通过依赖倒置(Dependency Inversion),实现业务逻辑与外部世界的彻底解耦,并利用现代化的状态管理和路由框架,优雅地将它们粘合起来。”
这个思想可以拆解为三个关键原则:
-
同心圆法则 (The Dependency Rule): 这是 Clean Architecture 的灵魂。内层(Domain)绝对不能依赖外层(Infrastructure, Presentation)。所有依赖关系都指向内部。这意味着,你的核心业务逻辑(比如商品折扣如何计算)不应该知道它是被一个 Flutter App 使用,数据是来自一个 REST API,还是存储在 Hive 数据库里。
-
职责分离原则 (Separation of Concerns):
- Clean Architecture 负责 “结构” 的分离:它定义了代码应该放在哪里(Domain, Application, Presentation, Infrastructure)。
- Riverpod 负责 “状态与依赖” 的分离:它作为依赖注入(DI)容器和服务定位器(SL),将各层实现“注入”到需要它们的地方;同时,它作为状态管理(SM)框架,将 UI 状态与 UI 渲染分离。
- Go_Router 负责 “UI 与导航” 的分离:它将页面导航逻辑从 UI 组件的调用中抽离出来,变为中心化的、基于路由地址的声明式导航,极大降低了页面间的耦合。
- Retrofit/Dio 负责 “业务与数据获取” 的分离:它将 HTTP 请求的构造和解析细节封装起来,让我们的数据层实现更专注于“获取什么数据”,而不是“如何获取”。
-
面向接口编程 (Programming to an Interface): 这是实现依赖倒置的具体手段。应用层(Application)和基础设施层(Infrastructure)都依赖于领域层(Domain)中定义的抽象接口(Repository Interfaces),而不是具体的实现。这使得我们可以轻松地替换数据来源(比如从网络 API切换到 Mock 数据或本地数据库)而无需修改任何业务逻辑或 UI 代码。
一言以蔽之:我们的目标是构建一个稳定的“业务核心”(Domain + Application),外部的变化(UI框架升级、数据库更换、API变更)不会轻易撼动这个核心。
第二部分:深入阐述各关键点与痛点分析
现在,我们深入到具体的“术”。
2.1 Clean 架构分层详解
一个典型的 Flutter Clean Architecture 项目结构如下:
-
Domain Layer (领域层):
- 职责: 包含最核心、最纯粹的业务逻辑和业务实体,完全独立于任何框架。
-
包含内容:
-
Entities (实体): 代表业务对象的类,如
User
,Product
,Order
。它们可以包含只依赖自身属性的业务逻辑(例如,一个Order
实体可以有一个isCompleted()
的 getter 方法)。 -
Repositories (仓库接口): 抽象接口,定义了获取和操作业务实体的数据契约。例如,
abstract class ProductRepository { Future<Product> getProductById(String id); }
。它只定义“做什么”,不定义“怎么做”。 -
Value Objects (值对象): 如
Email
,Password
等,用于封装验证逻辑,保证数据的有效性。
-
Entities (实体): 代表业务对象的类,如
-
特点: 无任何 Flutter/Dart 外部库依赖(除了可能的
equatable
等辅助库)。这是最稳定、最可移植的一层。
-
Application Layer (应用层 / Use Cases):
- 职责: 编排和调度 Domain 层和 Infrastructure 层,执行具体的应用功能。它代表了“用户想要做什么”。
-
包含内容:
-
Use Cases (或称 Interactors): 每个 Use Case 代表一个单一的应用功能点。例如
GetProductDetailsUseCase
,AddToCartUseCase
。它们会调用一个或多个 Repository 接口来完成任务。
-
Use Cases (或称 Interactors): 每个 Use Case 代表一个单一的应用功能点。例如
-
特点: 依赖于 Domain 层。它不知道 UI,也不知道数据来自网络还是本地。它只负责协调。例如,
AddToCartUseCase
的execute
方法可能会先调用ProductRepository
获取商品信息,再调用UserRepository
检查用户资格,最后调用CartRepository
将商品加入购物车。
-
Presentation Layer (表现层):
- 职责: 显示 UI,响应用户交互,并将用户行为传递给 Application 层。
-
包含内容:
-
Widgets/Pages (UI): Flutter 的
StatelessWidget
或ConsumerWidget
。它们应该尽可能“笨”,只负责根据状态渲染 UI 和转发用户事件。 -
State Management (Providers/ViewModels): Riverpod Providers 就在这里扮演核心角色。通常我们会为每个页面或复杂组件创建一个
StateNotifierProvider
或AsyncNotifierProvider
,它扮演着 ViewModel/Controller 的角色。这个 Provider 会调用相应的 Use Case,处理返回结果(成功、失败、加载中),并管理 UI 所需的状态。 - Navigation (Go_Router): 路由配置和导航逻辑。
-
Widgets/Pages (UI): Flutter 的
-
Infrastructure Layer (基础设施层):
- 职责: 实现 Domain 层定义的接口,处理所有与外部世界的交互。
-
包含内容:
-
Repository Implementations: 对 Domain 层 Repository 接口的具体实现。例如,
ProductRepositoryImpl
会实现ProductRepository
接口,其内部会调用一个或多个数据源。 -
Data Sources:
-
Remote Data Source: 使用
Retrofit
和Dio
来访问网络 API。 -
Local Data Source: 使用
Hive
,Isar
,shared_preferences
等进行本地数据持久化。
-
Remote Data Source: 使用
- Other Services: 封装其他平台相关的功能,如设备信息、权限管理、推送通知、分析服务等。
-
Repository Implementations: 对 Domain 层 Repository 接口的具体实现。例如,
2.2 Riverpod 的角色与层次归属
这是一个常见困惑点。Riverpod 不属于任何单一层,而是贯穿各层的“粘合剂”和“电力系统”。
-
在 Presentation 层:
- UI 组件 (
ConsumerWidget
) 通过ref.watch
来订阅 Provider 暴露的 UI 状态,实现响应式刷新。 - 用户事件(如按钮点击)通过
ref.read
或ref.notifier
来调用 Provider 中的方法,从而触发业务逻辑。
- UI 组件 (
-
作为依赖注入 (DI) 容器:
-
核心作用: 在
main.dart
或应用启动时,我们通过ProviderScope
和override
来“组装”我们的应用。 -
示例:
// Domain Layer (product_repository.dart) abstract class ProductRepository { ... } // Infrastructure Layer (product_repository_impl.dart) class ProductRepositoryImpl implements ProductRepository { ... } // Application Layer (get_product_usecase.dart) class GetProductDetailsUseCase { final ProductRepository _repo; GetProductDetailsUseCase(this._repo); ... } // Riverpod Providers (providers.dart) // 1. 提供基础设施层的具体实现 final productRepositoryProvider = Provider<ProductRepository>((ref) { // 在这里可以根据环境返回 Mock 或真实实现 return ProductRepositoryImpl(ref.watch(dioProvider)); }); // 2. 提供应用层的 UseCase,并自动注入依赖 final getProductDetailsUseCaseProvider = Provider<GetProductDetailsUseCase>((ref) { // Riverpod 自动处理依赖关系! final repository = ref.watch(productRepositoryProvider); return GetProductDetailsUseCase(repository); }); // 3. 提供表现层的 StateNotifier/ViewModel final productDetailsViewModelProvider = StateNotifierProvider.family<...>((ref, productId) { // ViewModel 可以访问 UseCase final useCase = ref.watch(getProductDetailsUseCaseProvider); return ProductDetailsViewModel(useCase, productId); });
- 通过这种方式,
ProductDetailsViewModel
依赖GetProductDetailsUseCase
,后者又依赖ProductRepository
接口。但在运行时,Riverpod 悄悄地将ProductRepositoryImpl
这个具体实现注入了进来,完美实现了依赖倒置。
-
核心作用: 在
2.3 项目组件划分:以“四级存储”为例
在实际项目中,一个功能(比如获取用户信息)可能涉及多级数据源。我们可以设计一个经典的四级存储策略:
-
一级缓存 (Memory Cache):
-
实现: Riverpod Provider 本身。使用
AsyncNotifierProvider.autoDispose
或普通的AsyncNotifierProvider
配合keepAlive
就可以实现。数据存在内存中,速度最快,但随 Provider 销毁而失效。 - 场景: 页面内或短时间内的重复数据请求。
-
实现: Riverpod Provider 本身。使用
-
二级缓存 (Local Persistence Cache):
- 实现: 在 Repository 实现中引入本地数据源(如 Hive/Isar)。
- 场景: 跨应用会话的数据缓存,如用户配置、不常变动的列表数据。启动应用时可以先从这里加载,给用户即时反馈。
-
三级存储 (Network):
-
实现: 远程数据源,通过
Retrofit
调用 API。 - 场景: 数据的最终来源(Source of Truth)。
-
实现: 远程数据源,通过
-
四级存储 (Bundled/Pre-populated Data):
-
实现: 打包在 App
assets
里的 JSON 或数据库文件。 - 场景: 初始配置、地区列表、默认数据等。
-
实现: 打包在 App
在 RepositoryImpl
中的逻辑流可能是这样的:
Future<User> getUser(String userId) async {
// 1. 尝试从内存缓存获取 (由 Riverpod 的 Provider 缓存策略管理)
// Riverpod 自身就处理了这部分,如果 provider 还在,就不会重新执行 fetch 逻辑
// 2. 尝试从本地数据库获取
final localUser = await _localDataSource.getUser(userId);
if (localUser != null && !isStale(localUser.timestamp)) {
return localUser;
}
// 3. 从网络获取
try {
final remoteUser = await _remoteDataSource.fetchUser(userId);
// 成功后,更新本地数据库
await _localDataSource.saveUser(remoteUser);
return remoteUser;
} catch (e) {
// 网络失败,如果本地有旧数据,也可以考虑返回
if (localUser != null) return localUser;
// 实在没有,就抛出异常
throw e;
}
}
第三部分:痛点识别与解决方案
这是架构实践中最具挑战性的部分。
痛点1:如何区分业务逻辑?(Domain Logic vs. Application Logic)
- 困惑: 一段逻辑,是应该放在 Entity 里,还是放在 Use Case 里?
-
解决方案与心法:
-
问自己一个问题:“这个逻辑是否具有普适性,并且只依赖于实体自身的数据?”
-
是 -> 放入 Domain Entity。 例如:
Order
实体有一个totalPrice
属性,一个calculateTotalPrice()
方法根据其lineItems
列表计算总价。这个计算逻辑是Order
固有的,不依赖外部服务。 -
否 -> 放入 Application Use Case。 例如:
ApplyCouponToOrderUseCase
。这个逻辑需要:1. 获取Order
;2. 获取Coupon
信息(可能要调CouponRepository
);3. 验证优惠券是否适用于该订单和用户(可能要调UserRepository
);4. 最后计算出新的价格。这个过程是在协调多个实体和仓库,它是一个应用级别的操作,因此属于 Use Case。
-
是 -> 放入 Domain Entity。 例如:
-
简单法则:
- Domain Logic: 规则和计算。
- Application Logic: 流程和编排。
-
痛点2:如何划分模块?(Monolith vs. Feature-based Modules)
-
困惑: 项目变大后,
lib
文件夹变得臃肿不堪。所有功能的代码混在一起,common
或shared
文件夹成为“垃圾场”。 -
解决方案:功能驱动的垂直切分 (Vertical Slicing by Feature)
-
放弃按层级划分顶级目录 (
data
,domain
,presentation
)。 这种方式在小项目里还行,大项目里会导致你为了修改一个功能,不得不在三个相距甚远的文件夹里跳来跳去。 - 拥抱按功能划分模块。 这是现代大型应用架构的趋势。
-
放弃按层级划分顶级目录 (
第四部分:以一个“可扩展的电商App”为例进行实战演练
项目背景: 一个电商 App,初期只有商品浏览、购物车、用户中心。未来需要快速迭代,加入直播带货、社区分享等新功能。
目标: 设计一个能够支撑这种演进的架构。
项目结构 (采用功能驱动模块化):
flutter_ecommerce_app/
├── lib/
│ ├── **features/** # 核心:按功能划分的模块
│ │ ├── **auth/** # 认证模块
│ │ │ ├── presentation/
│ │ │ │ ├── screens/login_screen.dart
│ │ │ │ └── providers/auth_providers.dart
│ │ │ ├── application/
│ │ │ │ └── usecases/login_usecase.dart
│ │ │ ├── domain/
│ │ │ │ ├── entities/auth_token.dart
│ │ │ │ └── repositories/auth_repository.dart (interface)
│ │ │ └── infrastructure/
│ │ │ ├── datasources/auth_remote_datasource.dart
│ │ │ └── repositories/auth_repository_impl.dart
│ │ ├── **products/** # 商品模块 (列表、详情)
│ │ │ └── ... (同样遵循 presentation/app/domain/infra 结构)
│ │ ├── **cart/** # 购物车模块
│ │ │ └── ...
│ │ └── **profile/** # 用户中心模块
│ │ └── ...
│ │
│ ├── **core/** # 跨功能共享的核心代码
│ │ ├── **domain/** # 共享的 Domain (e.g., User, AppError)
│ │ │ └── entities/user_entity.dart
│ │ ├── **ui/** # 共享的 UI 组件 (e.g., PrimaryButton, LoadingIndicator)
│ │ ├── **network/** # 共享的网络配置 (Dio instance, interceptors)
│ │ │ └── dio_client.dart
│ │ ├── **navigation/** # 共享的导航配置
│ │ │ └── app_router.dart (Go_Router 配置)
│ │ ├── **storage/** # 共享的存储封装 (e.g., Hive helper)
│ │ └── **utils/** # 共享的工具类 (e.g., formatters, validators)
│ │
│ ├── **main.dart** # 应用入口,组装 ProviderScope 和 GoRouter
│ └── **injection_container.dart** # (可选) 集中管理所有顶层 Provider 的声明
│
└── pubspec.yaml
这个结构的优势:
-
高内聚:
auth
相关的所有代码都在features/auth
目录下,从 UI 到数据源一目了然。 -
低耦合:
-
products
模块不直接依赖cart
模块。如果需要交互(比如“添加到购物车”),products
模块的AddToCartUseCase
会调用CartRepository
接口。这个接口的实现在cart
模块中,但依赖关系是products
->cart/domain
,而不是products
->cart/infrastructure
,耦合度很低。 -
可移除性: 如果老板说“我们不要购物车功能了”,理论上你可以直接删除
features/cart
文件夹,修复一下路由和调用点,应用主体依然能运行。
-
-
可扩展性:
- 当需要加入“直播带货”功能时,只需新建一个
features/live_streaming
模块,按照同样的结构进行开发。 - 新模块可以复用
core
里的所有组件,如core/network
的 Dio 实例,core/ui
的按钮等。
- 当需要加入“直播带货”功能时,只需新建一个
-
团队协作: 不同的团队可以并行开发不同的 feature 模块,冲突仅限于
core
和pubspec.yaml
,大大提高了开发效率。
Go_Router 在此结构中的作用:
在 core/navigation/app_router.dart
中,你会定义所有路由:
final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => ProductsListScreen(), // from products feature
),
GoRoute(
path: '/product/:id',
builder: (context, state) => ProductDetailsScreen(id: state.pathParameters['id']!), // from products feature
),
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(), // from auth feature
),
GoRoute(
path: '/cart',
builder: (context, state) => CartScreen(), // from cart feature
),
],
);
这样,ProductsListScreen
只需调用 context.go('/product/123')
,而无需知道 ProductDetailsScreen
的存在,实现了导航解耦。
总结
作为架构师,我们的工作不是选择最“时髦”的技术,而是构建一个能够应对未来不确定性的、有弹性的系统。
- Clean Architecture 提供了抵御变化的“防火墙”。
- 功能模块化 提供了应对业务增长的“扩展坞”。
- Riverpod 提供了连接一切的、灵活高效的“能源和通信网络”。
- Go_Router 和 Retrofit 则是这个网络中负责特定任务(导航、网络)的、可替换的“标准化插件”。
下一篇, 将在此基础上, 讲解如何开发 User 模块.