7.2 KiB
02. 分层架构规范
决策
每个 feature_* 包内部采用简化版分层,domain 层可选,不强制每个 feature 都有:
feature_xxx/
lib/
feature_xxx.dart # 对外唯一导出文件
src/
presentation/ # widgets + Riverpod provider/notifier
domain/ # 可选:entity + repository 接口 + use case
data/ # repository 实现 + remote/local datasource
test/
各层职责
- presentation:widgets、Riverpod
Notifier/Provider。只处理 UI 状态和用户交互,不直接调用data层的具体实现类,通过依赖注入拿到抽象类型。 - domain(可选):
entity定义业务模型,repository接口声明数据契约,use case封装跨 repository 协调或多步骤业务规则。 - data:
repository接口的具体实现,内部再拆remote_datasource(走core_network)和local_datasource(走core_storage)。
何时可以跳过 domain 层
判断标准:
- 可以跳过:功能是简单 CRUD、没有跨 repository 协调、没有多步骤业务规则——
presentation直接依赖data层定义的 repository 接口即可,repository接口挪到data层里声明。 - 必须要有:涉及多步骤业务规则(如支付的多步校验)、需要协调多个 repository、包含状态机或需要独立于 UI 单元测试的核心业务逻辑——
repository接口放在domain,data层依赖domain反向实现接口。
Repository 接口的位置规则
- 有
domain层:接口定义在domain/repository/,data/repository_impl/实现它,presentation只依赖domain里的抽象类型。 - 无
domain层:接口直接定义在data/repository/,同文件或同目录下给出实现类,presentation依赖这个接口类型。
两种情况下,presentation 都不允许直接依赖 data 层的具体实现类(如 XxxRepositoryImpl),只依赖接口——这条不因为是否跳过 domain 层而改变。
跨层依赖规则
presentation → domain(或直接 → data 的接口,若跳过 domain)
domain → 不依赖 presentation / data
data → 依赖 domain 的接口(若有),依赖 core_network / core_storage
domain 层禁止 import 任何 Flutter SDK(package:flutter/...)——保持纯 Dart,可脱离 UI 单独做 unit test。
附录:分层架构是什么,为什么要分层
给还没接触过这套分层习惯的同学看的入门说明。
要解决的问题
如果 UI 代码里直接写网络请求、直接 new 一个 Dio 实例、直接操作数据库——短期能跑,但会导致两个问题:
- 没法单独测试业务逻辑:想验证"支付金额校验规则对不对",得连 widget 一起跑测试,跑得慢还容易因为 UI 变了导致业务逻辑测试跟着挂。
- 换底层实现要动 UI 代码:比如把网络库从
dio换掉,或者把本地存储从shared_preferences换成Drift,如果 UI 直接依赖具体实现类,改动会散落得到处都是。
分层的本质:把"业务规则"和"业务规则的具体实现方式(用什么网络库、存什么数据库)"分开,中间用抽象接口隔开。这就是 Clean Architecture 这套思想的核心,我们只取最简化的三层版本,不套用它完整的同心圆规则。
依赖方向是关键
三层最重要的不是"分了几层",而是依赖只能单向流动:
presentation ──依赖──> domain ──定义接口,不依赖任何人
▲
│ 实现接口(依赖倒置)
data
domain 不 import data,也不 import presentation——它甚至不知道 data 层是用 dio 还是别的什么网络库实现的,只定义"我需要一个能拿到 PaymentOrder 的东西"(接口),至于这个东西具体怎么实现,由 data 层负责,这就是依赖倒置原则。好处是:domain 层的业务规则可以完全脱离网络、脱离 UI 单独写单元测试。
示例一:有 domain 层(feature_payment,支付确认——多步骤业务规则)
// domain/entity/payment_order.dart
class PaymentOrder {
final String orderId;
final int amountCents;
final PaymentStatus status;
const PaymentOrder({required this.orderId, required this.amountCents, required this.status});
}
// domain/repository/payment_repository.dart
abstract class PaymentRepository {
Future<PaymentOrder> fetchOrder(String orderId);
Future<void> confirmPayment(String orderId, String pinToken);
}
// domain/use_case/confirm_payment_use_case.dart
class ConfirmPaymentUseCase {
final PaymentRepository _repository;
ConfirmPaymentUseCase(this._repository);
Future<void> call(String orderId, String pinToken) async {
final order = await _repository.fetchOrder(orderId);
if (order.status != PaymentStatus.pending) {
throw StateError('订单状态不允许支付: ${order.status}');
}
if (order.amountCents <= 0) {
throw ArgumentError('订单金额非法');
}
await _repository.confirmPayment(orderId, pinToken);
}
}
// data/repository/payment_repository_impl.dart
class PaymentRepositoryImpl implements PaymentRepository {
final Dio _dio; // 来自 core_network
PaymentRepositoryImpl(this._dio);
@override
Future<PaymentOrder> fetchOrder(String orderId) async {
final res = await _dio.get('/orders/$orderId');
return PaymentOrder(
orderId: res.data['orderId'],
amountCents: res.data['amountCents'],
status: PaymentStatus.values.byName(res.data['status']),
);
}
@override
Future<void> confirmPayment(String orderId, String pinToken) =>
_dio.post('/orders/$orderId/confirm', data: {'pinToken': pinToken});
}
ConfirmPaymentUseCase 的多步校验规则可以直接用假的 PaymentRepository 实现来做单元测试,完全不需要启动 Flutter engine 或起一个 mock server。
示例二:跳过 domain 层(feature_store,门店列表——简单 CRUD)
// data/repository/store_repository.dart
abstract class StoreRepository {
Future<List<Store>> fetchNearbyStores(double lat, double lng);
}
class StoreRepositoryImpl implements StoreRepository {
final Dio _dio;
StoreRepositoryImpl(this._dio);
@override
Future<List<Store>> fetchNearbyStores(double lat, double lng) async {
final res = await _dio.get('/stores', queryParameters: {'lat': lat, 'lng': lng});
return (res.data as List).map((e) => Store.fromJson(e)).toList();
}
}
没有多步骤规则、没有跨 repository 协调,接口和实现直接放在 data 层,presentation 直接依赖 StoreRepository 这个接口,省掉一层 domain 目录和 use case 模板代码。