基础与工程篇-Flutter 工程目录如何设计才可长期维护

12 阅读4分钟

Flutter 工程目录如何设计才可长期维护

这是我 Flutter 实战系列第 2 篇。
目标很明确:不是“目录长什么样”,而是目录如何服务中大型项目的长期演进


1. 问题背景:业务场景 + 现象

当项目从 10 个页面增长到 100+ 页面后,最先崩的通常不是功能,而是结构:

  • 找代码靠搜索,不靠约定
  • 页面里堆网络请求、状态、埋点、业务判断
  • 同类逻辑在多个模块重复实现
  • 改一个接口,影响范围不透明
  • 新人上手慢,改动风险高

我在项目里也经历过这个阶段:业务推进很快,但目录和分层跟不上,导致“短期能跑,长期难维护”。


2. 原因分析:核心原理 + 排查过程

2.1 核心原理:目录不是“美观问题”,是“依赖治理问题”

一个目录结构是否可持续,关键看三件事:

  1. 边界是否清晰:UI、业务状态、数据访问是否职责分离
  2. 依赖是否单向:上层依赖下层,避免环依赖
  3. 变更是否局部:改一个需求,是否只改少量文件

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. 效果验证:数据 / 截图 / 日志

  1. 改动影响面对比

    • 重构前:一个需求改 12 个文件
    • 重构后:改 4~6 个文件,且集中在单模块
  2. 新人上手时长

    • 重构前:定位一个支付问题需要 2 小时
    • 重构后:20~30 分钟可定位主链路
  3. 重复代码下降

    • 同类请求封装、错误处理抽到统一层后重复率降低
  4. 线上回归风险下降

    • 因模块边界清晰,跨模块误伤显著减少

6. 可复用结论:通用经验 + 避坑清单

6.1 通用经验

  1. 先按业务切模块,再在模块内分层
  2. 页面层轻量化,业务逻辑收口到 ViewModel/UseCase
  3. common 目录宁小勿大,避免“公共垃圾场”
  4. 每个模块要有固定骨架,新人可预测
  5. 依赖方向只允许单向pages -> view-model -> server/model

6.2 避坑清单

  • 不要把所有通用代码都丢 utils
  • 不要让 common 反向依赖 modules
  • 不要在页面直接写 request
  • 不要把第三方 SDK 调用散落在各页面
  • 不要先追求“完美架构”,先把边界和规范立起来

我在项目中的落地建议(可当团队规范)

  • 每个业务模块必须包含:pagesview-modelstateserver
  • 页面文件建议控制在 300~500 行以内,超出就拆组件
  • 新增接口必须先定义 API 常量,再进 server 层
  • 禁止跨模块直接读对方 state(通过公共服务或明确接口)

小结

可长期维护的 Flutter 工程目录,本质不是“怎么摆文件夹”,而是:

  • 让依赖可控
  • 让变更可预测
  • 让协作可规模化

如果目录做对了,后续你会明显感受到:迭代速度更稳、Bug 更容易收敛、新人接手更顺畅。