Flutter 工程目录如何设计才可长期维护
这是我 Flutter 实战系列第 2 篇。
目标很明确:不是“目录长什么样”,而是目录如何服务中大型项目的长期演进。
1. 问题背景:业务场景 + 现象
当项目从 10 个页面增长到 100+ 页面后,最先崩的通常不是功能,而是结构:
- 找代码靠搜索,不靠约定
- 页面里堆网络请求、状态、埋点、业务判断
- 同类逻辑在多个模块重复实现
- 改一个接口,影响范围不透明
- 新人上手慢,改动风险高
我在项目里也经历过这个阶段:业务推进很快,但目录和分层跟不上,导致“短期能跑,长期难维护”。
2. 原因分析:核心原理 + 排查过程
2.1 核心原理:目录不是“美观问题”,是“依赖治理问题”
一个目录结构是否可持续,关键看三件事:
- 边界是否清晰:UI、业务状态、数据访问是否职责分离
- 依赖是否单向:上层依赖下层,避免环依赖
- 变更是否局部:改一个需求,是否只改少量文件
2.2 排查过程:我用 4 个信号判断“目录已经失控”
pages/文件变超大(动辄 600~1500 行)- “同名 util” 在多个目录出现
- 一个功能改动要跨 7~10 个目录
- 同一业务字段在页面、网络层、模型层被重复转换
满足其中 2 条以上,基本就该重构目录与分层了。
3. 解决方案:方案对比 + 最终选择
3.1 常见目录方案对比
方案 A:按技术分层(pages / widgets / models / services)
优点:
- 初期上手快
缺点: - 业务增大后,一个功能分散在很多目录里,追链路痛苦
方案 B:按业务模块(room / wallet / user)+ 模块内分层
优点:
- 功能内聚,定位快,适合多人协作
缺点: - 需要团队有统一规范,否则会“模块化外壳,内部混乱”
3.2 我最终选择
按业务模块组织 + 模块内分层统一模板,并做一条硬约束:
页面层不能直接依赖网络请求;必须经过 ViewModel/UseCase(或同等中间层)。
4. 关键代码:最小必要代码片段
以下是我实战中可长期维护的一套目录模板,你可以直接套。
4.1 推荐目录结构(可直接复制)
lib/
startup/ # 启动流程、初始化、全局注入
common/ # 跨模块公共能力(严格控制)
api/ # API path 常量
http/ # request、拦截器、错误处理
router/ # 路由封装
theme/ # 主题、样式令牌
utils/ # 真正通用的工具
modules/
wallet/
pages/ # 页面容器(只关心渲染和事件分发)
widgets/ # wallet 私有组件
view-model/ # 状态与业务编排
state/ # State 定义
server/ # 数据访问(HTTP/本地)
model/ # DTO/VO
enum/
dialog/
room/
pages/
widgets/
view-model/
state/
server/
model/
trtc/
gen/ # assets.gen.dart 等自动生成文件
4.2 页面只做“消费状态 + 触发动作”
class WalletPaidProductPage extends ConsumerWidget {
const WalletPaidProductPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(walletProvider);
final vm = ref.read(walletProvider.notifier);
return Column(
children: [
Text('当前余额: ${state.balance}'),
ElevatedButton(
onPressed: () => vm.fetchProducts(),
child: const Text('刷新商品'),
),
],
);
}
}
4.3 ViewModel 承接业务流程(而不是页面)
class WalletViewModel extends StateNotifier<WalletState> {
WalletViewModel() : super(WalletState.initial());
Future<void> fetchProducts() async {
final result = await WalletServer.getPaidProductList();
state = state.copyWith(products: result?.packages ?? []);
}
Future<void> submitApplePay(String productId) async {
final order = await OrderServer.applePayOrder(
paymentModel: OrderRequestModel(productId: productId),
);
state = state.copyWith(lastOrderNo: order?.orderNo ?? '');
}
}
4.4 Server 层只关心“数据获取与解析”
class WalletServer {
static Future<WalletPaidProductModel?> getPaidProductList() async {
final response = await request('/system/api/recharge/diamond/packages');
return WalletPaidProductModel.fromJson(response);
}
}
5. 效果验证:数据 / 截图 / 日志
-
改动影响面对比
- 重构前:一个需求改 12 个文件
- 重构后:改 4~6 个文件,且集中在单模块
-
新人上手时长
- 重构前:定位一个支付问题需要 2 小时
- 重构后:20~30 分钟可定位主链路
-
重复代码下降
- 同类请求封装、错误处理抽到统一层后重复率降低
-
线上回归风险下降
- 因模块边界清晰,跨模块误伤显著减少
6. 可复用结论:通用经验 + 避坑清单
6.1 通用经验
- 先按业务切模块,再在模块内分层
- 页面层轻量化,业务逻辑收口到 ViewModel/UseCase
- common 目录宁小勿大,避免“公共垃圾场”
- 每个模块要有固定骨架,新人可预测
- 依赖方向只允许单向:
pages -> view-model -> server/model
6.2 避坑清单
- 不要把所有通用代码都丢
utils - 不要让
common反向依赖modules - 不要在页面直接写 request
- 不要把第三方 SDK 调用散落在各页面
- 不要先追求“完美架构”,先把边界和规范立起来
我在项目中的落地建议(可当团队规范)
- 每个业务模块必须包含:
pages、view-model、state、server - 页面文件建议控制在
300~500行以内,超出就拆组件 - 新增接口必须先定义 API 常量,再进 server 层
- 禁止跨模块直接读对方 state(通过公共服务或明确接口)
小结
可长期维护的 Flutter 工程目录,本质不是“怎么摆文件夹”,而是:
- 让依赖可控
- 让变更可预测
- 让协作可规模化
如果目录做对了,后续你会明显感受到:迭代速度更稳、Bug 更容易收敛、新人接手更顺畅。