ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flutter项目结构全解析:从目录分层到工程化配置

Flutter项目结构全解析:从目录分层到工程化配置 接过别人做了一半的 Flutter 项目是我这几年最常遇到的场景之一。压缩包解开lib目录下有二十多个.dart文件散在根层级main.dart一千多行网络请求、页面跳转、本地缓存、弹窗全挤在里面assets里图片按1.png、2.png编号命名。它能跑但你让我加一个新模块我得先把那个一千行的文件从头读到尾。还有一个反向的场景自己从flutter create开始做一个新项目第一天很爽第二十天开始怀疑人生——不知道该把新写的文件放在哪同一个功能的相关代码分居两地删一个旧功能要翻五个目录确认没有残留。这两个场景指向同一个问题一个完整的 Flutter 项目构成它的不只是能编译通过的代码而是一套让第二个人、第二十天之后的你自己还能顺畅接手的文件组织秩序。目录怎么分、根目录那些不认识的文件各自管什么、资源放哪、路由写在哪、测试和构建配置怎么和主代码对齐——这些看起来是工程规范实际上是每天都要打交道的东西。下面我按从项目根目录往里走的顺序把一套完整 Flutter 项目的构成拆开讲一遍包括每个文件为什么存在、为什么这么分、以及我在实际搭建时踩过的那些坑。1. 拆开一个能跑的工程根目录里每个文件到底管什么执行flutter create之后你不写一行代码根目录就已经有十几个条目了。新手最常见的处理方式是认识的留着不认识的不管.metadata、.gitignore、analysis_options.yaml这三个常年躺在那里没人碰。但你一旦开始接别人的项目、或者项目要在多台机器和流水线上跑这些没人碰的文件就会跳出来咬你一口。我先把它们的职责逐个说清楚。1.1 pubspec.yaml 的三个身份依赖表、资源清单、约束声明大部分人把pubspec.yaml当成一个依赖列表来用需要什么包就flutter pub add一下完事。它实际承担三件事而且后面两件出问题的概率更高。第一是依赖与版本约束。这里的关键不是写了哪些包而是版本约束的写法。^1.2.3这种 caret 写法允许次版本号浮动对应用项目来说通常是可以接受的但如果你的项目依赖某个包的行为非常精确比如某个图表库的渲染细节、某个序列化库对 null 的处理把它写成1.2.3锁定反而更安全。同时别忘了environment段里的 SDK 约束——这条决定了这个项目能被哪个版本的 Dart 工具链打开团队里有人用旧版本 IDE 打不开项目八成是这里卡住了。第二是资源声明。flutter段下的assets列表决定了哪些文件会被打进产物。这里有一个几乎所有新手都踩过的坑声明目录时结尾的斜杠。写- assets/images/会把该目录下的文件包含进来写- assets/images只会包含这个路径本身而它是个目录所以实际上什么也没包含进去。区别只有一个字符报错信息还非常隐晦——运行时图片加载失败控制台给你一句Unable to load asset你回头检查文件名拼写检查半小时。更麻烦的是这个列表不会递归assets/images/icons/下的文件必须单独声明一行很多人以为声明了父目录就自动包含子目录结果子目录里的图全部加载不出来。第三是项目元信息与构建开关。name决定了包名也决定了所有内部 import 的前缀package:your_app/...改它的成本比想象中高version里的后面那串 build number 是提交应用市场时的版本号依据很多人只改前面的语义化版本忘了后面那串。uses-material-design: true这条如果不写Icons.xxx系列图标全部渲染成空白方块这是个非常经典的排查点。字体声明fonts段则在自定义字体场景下必不可少每个 weight 都要单独列一项漏一个就会导致FontWeight.w600渲染成默认字重。提示pubspec.lock要提交到版本库应用项目但不要手动编辑它。团队里两个人pubspec.lock冲突时正确做法是取其中一份再重新flutter pub get而不是手工合并那些依赖树。1.2 平台壳工程 android/ios/web 的边界在哪里android/、ios/、web/、macos/、linux/、windows/这六个目录本质上是同一个 Dart 应用在不同平台上的壳。它们负责的是 Flutter 管不到的部分应用图标、启动页、权限声明、签名配置、原生插件注册、深链配置。很多纯 Flutter 开发者对这几个目录有心理阴影看到build.gradle就想绕道走但只要理解它们的边界日常需要动的就那么几处。Android 侧的常改点集中在android/app/build.gradle现在新项目多半是build.gradle.ktsapplicationId、minSdkVersion、targetSdkVersion、签名配置、以及buildTypes里的混淆开关。权限相关的声明在android/app/src/main/AndroidManifest.xml注意 Flutter 项目通常有三个 Manifest 文件main、debug、profile网络权限在 debug 版里默认是开着的release 版需要自己确认——这是调试一切正常打包上线后接口全挂的头号原因。iOS 侧的关键文件是ios/Runner/Info.plist权限描述文案、URL Scheme、ATS 配置和ios/Podfile原生依赖的版本约束、平台最低版本。Podfile里那行platform :ios, 12.0如果和某个插件要求的最低版本不匹配pod install就会失败报错信息指向一堆你不认识的原生库。这种时候先看报错里那句required a higher minimum deployment target再去改这行数字比盲试快得多。Web 侧相对简单web/index.html控制页面外壳、title、以及首屏加载的占位内容。这里有个细节Flutter Web 首次加载会有一段白屏引擎在下载在index.html里放一个简单的 loading 遮罩能显著改善观感成本却极低。关于这些目录要不要提交到版本库——要。它们是项目的一部分尤其是原生侧任何一次改动图标、权限、签名都必须被记录否则换台机器打包出来的产物就不一样。1.3 analysis_options.yaml 与 .gitignore不出声但决定项目寿命analysis_options.yaml这个文件的存在感极低但它决定了整个项目的代码风格是否统一。新项目默认引入flutter_lints包包含一套官方推荐的规则集。我的建议是在项目第一天就把团队约定写进去而不是等到代码写了三个月再来批量修 lint——后者会产生一个几百文件的巨型改动评审没法看最后往往不了了之。几条我实际会打开或关闭的规则prefer_const_constructors打开对性能有实际收益且能培养习惯、avoid_print打开防止调试用的 print 混进生产代码、prefer_single_quotes按团队习惯、always_specify_types建议关闭在 Dart 的强类型推断下到处写类型反而是噪音。此外可以针对特定目录做规则豁免比如生成的 l10n 文件、.g.dart生成文件用analyzer: exclude:把它们排除在分析之外能大幅减少分析器噪音。.gitignore的策略更简单粗暴build/、.dart_tool/、.flutter-plugins、*.iml这些必须忽略pubspec.lock必须提交。ios/Pods/和ios/.symlinks/也要忽略但ios/Podfile.lock建议提交它保证团队每个人的原生依赖版本一致。还有个小细节.metadata这个文件记录了项目创建时的工具链版本信息flutter命令会读写它不要手动编辑也不要轻易在冲突时删除删掉后某些工具会认为这是一个非标准工程。2. lib 目录的第一次分层为什么最后都会收敛到 feature-firstlib是整个项目的核心也是分层方案争论最激烈的地方。我见过三种典型状态一是所有文件平铺在lib根下靠文件名区分二是按技术层切成models/、views/、services/、utils/几大块三是按业务功能切成features/login/、features/order/、features/profile/。第一种在文件超过三十个之后必然崩盘第二种能撑到中等规模但在业务交叉时开始难受第三种是我目前所有新项目的默认选择。这一节讲清楚为什么。2.1 main.dart 只该干三件事很多项目烂掉的起点就是main.dart。它一开始只有五行然后有人加了个全局配置初始化有人加了个路由表有人把主题也放进来半年后它变成一个八百行、import 了四十个文件、谁都不敢动的东西。我的做法是main.dart只保留三件事——绑定初始化、错误捕获、把控制权交给 bootstrap。具体来说import package:flutter/material.dart; import bootstrap.dart; void main() async { WidgetsFlutterBinding.ensureInitialized(); await bootstrap(); }WidgetsFlutterBinding.ensureInitialized()这行必须在任何依赖平台通道的操作之前调用否则会抛出Binding has not yet been initialized。本地数据库初始化、屏幕方向锁定、SharedPreferences 预热都属于这一类它们全部下沉到bootstrap()里。bootstrap.dart则承担注册依赖 → 读取配置 → 决定首屏 → runApp这条链路它是唯一知道全局装配顺序的文件。为什么要把这段拆出来因为它可测。直接写在main()里的初始化流程单元测试没法调用main会runApp测试环境里跑不起来。拆成bootstrap()之后你可以在集成测试里先跑一遍装配、注入一个 mock 的配置源再验证首屏路由的决策逻辑。这个收益在项目后期排查为什么冷启动偶发白屏这类问题时体现得非常明显。顺带一提app.dart它返回MaterialApp.router或MaterialApp只负责装配主题、路由、本地化代理、全局 Provider 作用域。页面的具体内容一律不写在这里。这样应用级配置和页面级实现的边界就非常清晰改主题不会碰到业务代码加页面也不会碰应用配置。2.2 按层切和按功能切的真实差异这两种切法的差异不在于哪个更规范而在于你日常改一个需求时需要同时打开几个目录。我用一张表说明对比维度按技术层切models/views/services按功能切features/xxx新增一个功能在 3~5 个目录各加文件在 1 个目录内加文件删除一个功能需全局搜索确认无残留删除整个目录即可新人理解某个业务需跨目录拼凑上下文打开一个目录即可通读跨功能复用天然共享都在同一层需显式下沉到 shared/core团队协作冲突多人同时改同一层目录冲突集中在各自 feature适合规模小型、功能单一的 Demo中大型、多模块并行的产品按层切的核心问题是耦合度不可见。models/里有五十个模型你从目录结构上完全看不出OrderModel只被订单模块使用、而UserModel被八个模块依赖等你想删订单模块时才发现动不了。按功能切之后一个 feature 目录内的依赖是内聚的跨 feature 的依赖必须走shared/或core/这个必须显式走出去的动作恰恰让耦合变得可见可审计。按功能切的代价也很实在三个功能都需要一个带圆角的头像组件时你得决定它放哪。我的经验是设一条明确的阈值——被两个以上 feature 使用、且不含业务语义的组件下沉到shared/widgets/含业务语义的比如订单状态标签留在 feature 内宁可复制一次。强行把带业务的组件抽到公共层后期会因为不同 feature 的需求分化而变成一堆if-else开关这比复制一份代码的代价大得多。2.3 一份可以直接抄的目录树具体落地的目录结构长这样我把它当模板用了好几个项目改动的都是细节lib/ ├── main.dart # 入口只做绑定与 bootstrap ├── bootstrap.dart # 依赖注册、配置加载、首屏决策 ├── app.dart # MaterialApp.router装配主题/路由/l10n ├── core/ # 与业务无关、全项目共享的能力 │ ├── constants/ # 常量、环境枚举、存储 key │ ├── errors/ # 异常类型、失败封装 │ ├── network/ # Dio 客户端、拦截器、基地址 │ ├── storage/ # 本地存储封装 │ ├── theme/ # 颜色、字体、组件主题 │ ├── router/ # 路由表与路由守卫 │ ├── utils/ # 无状态工具函数、扩展方法 │ └── di/ # 依赖注册入口 ├── features/ │ ├── auth/ │ │ ├── data/ │ │ │ ├── datasources/ # 远端/本地数据源 │ │ │ ├── models/ # DTO 与序列化 │ │ │ └── repositories/ # 仓储实现 │ │ ├── domain/ │ │ │ ├── entities/ # 业务实体 │ │ │ ├── repositories/ # 仓储抽象 │ │ │ └── usecases/ # 用例 │ │ └── presentation/ │ │ ├── pages/ # 页面 │ │ ├── widgets/ # 该功能专属组件 │ │ └── state/ # 状态控制器 │ └── home/ ├── shared/ │ ├── widgets/ # 跨功能复用的纯 UI 组件 │ └── extensions/ # 跨功能复用的扩展 └── l10n/ # arb 文件若开启国际化这里要强调domain 层不是必须的。如果你做的是一个数据展示型应用业务逻辑就是拉接口、显示列表硬套三层架构会写出一堆只做转发的 usecase纯属自找麻烦。我的判断标准是这个功能有没有需要独立测试的业务规则——有就上 domain没有就data presentation两层保持轻量。架构是为维护成本服务的不是为图纸好看服务的。3. 状态管理与依赖注入如何落在目录结构上选状态管理框架的讨论已经够多了我更关心的是另一个问题选完之后代码放在哪。同样的 Riverpod 项目有人把所有 Provider 塞在一个providers.dart里有人每个 feature 一个state/目录六个月后的可维护性天差地别。目录结构其实是架构的 X 光片看一眼就知道这个项目有没有想清楚。3.1 目录结构其实是架构的 X 光片判断一个 Flutter 项目的架构成熟度我一般看三个信号。第一presentation/层里有没有直接 import 网络库或数据库包——如果有说明数据获取逻辑没有被隔离UI 和 IO 焊死在一起改接口要改页面。第二data/models/下的模型类有没有混进BuildContext、Widget之类的 UI 类型——如果有说明序列化模型和视图模型没分开后期接口字段一变就牵一发动全身。第三domain/里有没有 import Flutter 的任何包——理论上 domain 层应该是纯 Dart能在无 UI 环境下跑测试。这三个信号都指向同一件事依赖方向是否单向。健康的依赖流向是presentation → domain ← dataUI 依赖抽象数据实现依赖抽象两者互不知道对方。目录结构的作用就是让这个方向在物理上可见——你从presentation/pages/login_page.dart里看到一行import ../../data/repositories/auth_repository_impl.dart立刻就知道这里越界了连代码都不用读。反过来说如果目录结构只是把文件按名字分了类但依赖关系是网状乱窜的那这个分层就是文件夹分层除了让文件树好看之外没有任何约束力。真正的分层约束力来自代码评审时的红线以及把红线写进analysis_options.yaml或者团队文档里。3.2 三种主流状态管理的文件落位方式不同方案的落位习惯差别挺大我按自己实际用过的整理一下Provider / Riverpod 系核心是声明与消费分离。Provider 的声明文件我建议放在 feature 内的state/目录一个文件声明该 feature 的所有 Provider命名带上 feature 前缀authStateProvider、homeListProvider。跨 feature 共享的 Provider比如当前登录用户放在core/di/或shared/providers/。Riverpod 用代码生成时有.g.dart伴随文件记得在.gitignore里做决策——我倾向于不提交生成文件但在 CI 里先跑dart run build_runner build保证产物一致。Bloc 系天然是一个 feature 一组文件xxx_bloc.dart、xxx_event.dart、xxx_state.dart放在presentation/bloc/下即可。Bloc 的好处是文件命名强制暴露职责坏处是样板代码多一个小功能三四个文件。如果项目里 Bloc 数量超过二十个我建议做一层再抽象把重复的 loading/error 状态收进基类否则状态类会膨胀得没法看。setState 系别笑很多页面确实只需要setState。这种情况下不需要任何状态管理目录把StatefulWidget写进pages/即可。我见过为了架构统一把一个纯静态展示页也套上 Bloc 的三个文件管一个没有状态变化的页面纯属浪费。状态管理只在有跨组件状态共享需求时才引入这是原则。三种方案在目录上的共同点是状态控制器和页面放在同一个 feature 下不要全局集中。全局集中的后果是删一个功能时控制器还留在那个大文件里没人敢动。3.3 get_it 与 ProviderScope注册入口放在 bootstrap依赖注入的注册入口只有一处放在core/di/由bootstrap()调用。这里有两个高频问题。第一个是注册顺序。如果AuthRepository依赖ApiClient而ApiClient依赖AppConfig注册时必须先AppConfig、再ApiClient、最后AuthRepository否则运行时抛对象未注册。项目大了以后这个顺序很难维护我的做法是把注册按模块拆成registerCore()、registerAuth()、registerHome()几个函数在bootstrap()里按模块依赖顺序调用并且每个 feature 的注册函数写在它自己的目录下features/auth/di.dart由core/di汇总调用。这样删 feature 时注册代码也跟着目录一起删。第二个是懒加载与提前初始化。get_it的registerLazySingleton是第一次取用时才构造registerSingleton是注册时立刻构造。对网络客户端这种需要读配置的用懒加载对需要在启动阶段就完成预热的比如日志上报、崩溃捕获用立即构造并且await。这两者混用会导致一个经典现象某个服务在调试时正常、在冷启动首屏偶发报错因为首屏页面取用它的时候它的依赖还没就绪。Riverpod 的ProviderScope是另一套逻辑所有 override 在ProviderScope(overrides: [...])里声明测试时用ProviderScope(overrides: [apiClientProvider.overrideWithValue(mock)])注入假实现。这个机制比 get_it 更适合测试因为它是作用域化的测试之间互不影响。我的项目里常见组合是get_it 管基础设施网络、存储、日志Riverpod 管状态两套并存但职责不重叠。4. 资源、路由、本地化最容易堆在 lib 根目录的三类东西这三类东西有个共同特点它们不属于任何一个业务功能但又必须被业务功能引用。于是很多人图省事在lib根目录下建assets.dart、routes.dart、strings.dart三个大文件全部常量塞进去。前三个月很爽半年后这三个文件各上千行每次改动都提心吊胆。4.1 assets 的目录规划与 pubspec 声明陷阱资源目录我建议放在项目根和lib平级而不是lib里面——lib里的非 Dart 文件虽然能被打包但会让代码搜索变得嘈杂。标准结构assets/ ├── images/ # 位图资源 ├── icons/ # 图标svg 或 png ├── fonts/ # 字体文件 ├── animations/ # lottie / rive 动画 └── config/ # 静态 json 配置pubspec.yaml的声明有几个必须注意的点我把它们和对应的症状列成表写法实际效果典型症状- assets/images不含目录下任何文件图片全加载失败- assets/images/含该目录直属文件不含子目录子目录图片加载失败- assets/fonts/且在fonts:段单独声明字体按 weight 注册漏写 weight 时字重回退默认命名规范也值得定一下我习惯用前缀_用途_状态的格式比如icon_nav_home_active.png、bg_login_gradient.png。纯数字命名1.png、2.png的项目我基本可以断定它没有设计稿交接流程后续换皮肤时会非常痛苦。还有一个实操细节图片资源体积。Flutter 打包不会自动压缩位图一张 3MB 的背景图会原样进包。我的习惯是加一步构建前检查超过 500KB 的位图必须确认是否有必要能转 WebP 的优先转Flutter 全平台支持纯色图标优先用矢量flutter_svg。4.2 路由表集中化的收益路由有两种写法命令式Navigator.push(context, MaterialPageRoute(...))和声明式路由表。前者写起来快但三个月后你会收获一堆散落在各页面的跳转代码改一个页面路径要全局搜索。我现在的项目一律用集中路由表go_router或 Flutter 自带的RouterAPI放在core/router/。集中路由表的收益具体有三条。第一路径字符串只有一处定义用常量类包起来拼错会编译报错而不是运行时 404。第二路由守卫有统一入口登录态校验、权限拦截、深链参数解析都能在一个redirect回调里处理而不是每个页面开头写一遍if (!loggedIn) return;。第三页面结构可见打开路由文件就能看到整个应用有哪些页面、层级关系如何这对接手项目的人极其友好。路由文件我建议按层级拆而不是按数量拆router.dart只负责拼装routes/auth_routes.dart、routes/home_routes.dart各自声明本模块的路由用ShellRoute或嵌套路由组合。这样删模块时路由也跟着走。有一点必须提醒路由表里的路径是外部契约。如果应用有深链或者 Web 版本路径一改外部链接就全废了。所以路径命名要在项目初期定好规范比如统一小写短横线后期只增不改。4.3 l10n 的 arb 文件与代码生成链路国际化这块如果项目确定只做单语言可以完全跳过不要为了以后可能要而提前引入——它会给你带来一堆AppLocalizations.of(context)!的空安全噪音。但如果确定要做多语言越早引入越好因为后期把硬编码中文抽出来是个纯体力活而且容易漏。标准链路是这样l10n.yaml放在项目根配置 arb 目录、输出目录、模板文件lib/l10n/app_en.arb、app_zh.arb存放键值执行flutter gen-l10n新版 Flutter 在flutter run时会自动跑生成AppLocalizations。生成文件我建议放在lib/l10n/generated/并加进analysis_options.yaml的 exclude 列表避免 lint 噪音。arb 文件的组织有个经验按功能拆文件不要一个大app_en.arb塞所有文案。auth_en.arb、home_en.arb分开通过l10n.yaml的arb-dir配合多个模板文件组合或者干脆用一个文件但用前缀分区auth_loginTitle、home_greeting。前者更清晰后者更省事看团队规模选。键名的命名规范一定要在第一天定死loginTitle和login_title混用是后期最大的痛苦来源。5. test 目录、构建变体与流水线项目完整的最后一块拼图前面讲的都是应用本身怎么组织但一个完整的工程还包括它的外围测试怎么写、多环境怎么切、自动化怎么跑。这三块在很多项目里是缺失的缺失的代价不会立刻显现而是在第一次线上事故、第一次紧急热修复时集中爆发。5.1 test 目录与 lib 的镜像关系test/目录的结构应该镜像lib/。lib/features/auth/domain/usecases/login_usecase.dart对应的测试就是test/features/auth/domain/usecases/login_usecase_test.dart。这个镜像关系的好处是找测试不用想——你知道代码在哪就知道测试在哪。测试的层次我按投入产出比排个序domain 层的纯逻辑测试性价比最高跑得快、不依赖环境、能覆盖核心业务规则data 层的序列化测试次之重点测接口返回的畸形数据会不会导致崩溃widget 测试再次之只测关键的交互组件和状态切换不要试图用 widget 测试覆盖所有 UI 细节维护成本会超过收益集成测试最少只覆盖两三条主流程登录、下单、支付跑在真实设备或模拟器上。flutter create生成的test/widget_test.dart里那个计数器测试新项目第一件事就是删掉或者替换掉它没有任何价值留着只会让人以为这个项目有测试。测试目录里我还会单独建test/helpers/放 mock 数据、测试用的 Provider 覆盖、fake 实现避免每个测试文件重复造轮子。有个细节值得说test/和integration_test/是两个不同目录。前者跑在 Dart VM 上速度快flutter test就能跑后者跑在设备上flutter test integration_test/需要连设备或模拟器。CI 里我一般只跑前者作为必过门禁后者放在 nightly 或者发布前的手动触发。5.2 flavors 与多环境配置的落地开发、测试、生产三套环境配置不能硬编码在代码里。方案有两层编译期注入用--dart-define构建期配置用 Flutter flavors。--dart-define的用法是flutter run --dart-defineAPI_BASE_URLhttps://xxx代码里通过String.fromEnvironment(API_BASE_URL)读取。它的优点是简单、不需要改原生配置缺点是不能存在多个值同时在手每次切换要改命令而且不小心会把密钥打进产物所以密钥绝对不要用 dart-define 传那个不是加密的。flavors 则是在原生的构建系统里定义多个变体。Android 侧在build.gradle里配productFlavors { dev {} prod {} }可以给每个变体不同的applicationId后缀这样开发版和正式版能同时装在一台机器上这个体验提升非常明显iOS 侧配xcconfig文件。这条路配置成本高一些但能让图标、应用名、包名、签名全部区分开。我的实际做法是两者结合flavor 区分为dev/prod两套壳环境的细粒度配置接口地址、开关用--dart-define或者config/dev.json静态文件在启动时加载。配置读取统一收在core/config/下暴露一个AppConfig对象其他代码一律从它取绝不出现散落的String.fromEnvironment。5.3 CI 上的四条命令流水线不需要很复杂四条命令能兜住大部分问题flutter pub get dart run build_runner build --delete-conflicting-outputs # 若使用代码生成 flutter analyze --fatal-infos flutter testflutter analyze加--fatal-infos是关键默认情况下 info 级别的 lint 不会让命令失败加上这个参数之后风格问题也会阻断合并。这看起来很严格但它是让 lint 规则真正生效的唯一办法——只要有一条路可以绕过三个月后规则就形同虚设。构建产物那一步flutter build apk --flavor prod --dart-define...。这里有个坑如果项目用了代码生成freezed、json_serializable、riverpod_generator构建机上必须先跑build_runner否则报找不到 xxx.g.dart。如果本地提交了生成文件要注意 CI 上重新生成的结果和提交的可能不一致生成器版本不同所以我的建议是统一不提交生成文件CI 每次重新生成环境一致性更好。6. 从零搭骨架的顺序与几个反复踩到的坑讲了这么多构成要素最后说一下实际动手时的顺序。这个顺序很重要因为有些东西事后补成本极低有些东西事后改成本极高。6.1 我建议的搭建顺序第一步定包名和应用名。flutter create --org com.yourcompany --project-name your_app这一步定了之后applicationId、iOS Bundle ID、package:前缀全都跟着走。事后改包名的成本是重新创建一个项目再搬代码非常痛苦。所以哪怕你还没想好产品名也先把 org 和项目名想清楚。第二步配 lint 和目录骨架。analysis_options.yaml先落地core/、features/、shared/三个空目录建好哪怕里面只有一个.gitkeep。骨架先立起来的意义在于后面每写一个文件都有明确去处不会因为暂时先放这而产生技术债。第三步配主题和路由。core/theme/里定颜色、字号、圆角、间距这几个基础 tokencore/router/里定路由表和守卫。这两块是全局性的定得越早后面页面开发越统一。第四步配网络层和错误模型。core/network/里封装好客户端、拦截器、统一错误类型core/errors/定好失败分类网络失败、业务失败、解析失败。这块定得早各个 feature 的数据层就能按统一模式写不会出现这个模块用 try-catch 返回 null那个模块抛异常的混乱。第五步再开始写第一个 feature。写完第一个 feature 之后回头检查一次目录结构把不合适的地方调整掉。第一个 feature 是最好的压力测试它会把骨架的问题全部暴露出来。此时改动成本很低等到五个 feature 写完再调整就是重构了。第六步补测试和 CI。在只有一两个 feature 的时候把测试和流水线打通成本最低。等代码量大了再补人会本能地抵触。6.2 五个反复出现的坑坑一assets目录声明的斜杠。前面提过了但它值得再强调一次因为它的症状图片加载失败和原因少一个斜杠之间隔得太远新人平均要花半小时才能定位。我的做法是在pubspec.yaml里给资源声明加注释写明目录必须带结尾斜杠。坑二debug 能跑、release 崩。八成是混淆或者权限问题。Android release 构建默认开启代码压缩某些反射式的序列化、原生插件的类名会被裁掉需要在proguard-rules.pro里保留。另外 release 版的AndroidManifest.xml不合并 debug 版里的网络权限接口请求会静默失败。排查方法是先在本地跑一次flutter run --release把问题暴露在开发阶段而不是发布阶段。坑三.g.dart生成文件的冲突。多人协作时两个人同时改了带freezed的模型生成文件冲突出现在合并请求里。解决办法就是前面说的生成文件进.gitignoreCI 统一生成。切换过去的那一次要清一次缓存git rm --cached把所有已跟踪的生成文件移除。坑四feature 之间的循环依赖。features/order引用了features/user的实体features/user又引用了features/order的模型Dart 编译器不会直接报错但依赖关系变成了一团乱麻。我的处理方式是出现第一个跨 feature 引用时就把被引用的实体下沉到domain/entities/或者shared/models/同时约定feature 之间不允许直接 import写进评审清单。坑五lib根目录的文件不断堆积。只要lib根下允许存在业务文件就一定会有人往里丢。我的做法是lib根下只留main.dart、app.dart、bootstrap.dart三个文件任何新增文件必须进子目录。这条规则简单粗暴但执行成本极低效果立竿见影。我自己现在的习惯是新项目开完flutter create的第一件事不是写代码而是花一个小时把上面这套骨架搭完——lint、目录、主题、路由、网络层、配置读取。这一个小时看起来没有产出任何功能但它决定了这个项目在第三个月是能顺畅加需求还是每加一个需求都要先做一次考古。踩过的坑多了之后会发现一个完整的 Flutter 项目代码只是表面让代码能长期被改动的组织结构才是底下那层真正支撑它的东西。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表