Flutter .new——这是我私下给“新建Flutter项目”这个动作起的代号。每次敲下flutter create我都觉得一个项目真正开始了不是因为代码从这时候开始长而是因为这一瞬间会把一堆隐藏约定一次性生成出来。标题里的.new不是什么隐藏命令它代表我这些年沉淀下来的一套新项目流程环境检查、参数设计、目录规划、依赖选型以及第一轮工程质量配置。这篇内容适合两类人。一类是刚接触 Flutter、马上要创建第一个项目的朋友你可以把后面当成一份可以直接照做的清单另一类是已经写过一两个项目、但总感觉初始化阶段没做对的开发者我会把容易忽略的细节和排查思路摊开来讲。所有内容都来自真实项目里的反复试错不是命令行文档的复述。1. 为什么“新建项目”这个动作值得单独写一篇很多人觉得flutter create敲完就完事了剩下的直接开写。但恰恰是这一步决定了项目后面三个月的维护姿势。我见过太多次“新建时省事、维护时加倍还债”的情况所以先把这件事拆开看。1.1 一条 flutter create 到底替你做了什么新手最容易低估flutter create的产出。它不是一个空目录加一个 main.dart而是一整套多平台工程骨架Androidandroid/目录下已经有 Gradle 工程、build.gradle配置、MainActivity 入口iOS/macOSios/、macos/目录里是完整的 Xcode 工程结构Web 与桌面端web/、windows/、linux/各自有平台壳Dart 侧lib/main.dart入口、默认pubspec.yaml、测试目录test/工程规范analysis_options.yaml、.gitignore、README.md、.metadata都顺带生成。这些生成物不是装饰它们是一套隐藏契约。举个例子Android 侧的applicationId在这时候定下来后面上架市场想改包名就非常痛苦iOS 侧虽然可以后改 Bundle ID但会牵扯签名、描述文件、推送配置。也就是说flutter create把很多“这辈子不常改、改一次伤筋动骨”的信息一次性焊在了工程里。所以我把新建项目看作一个完整的工程活动而不是一条命令。你要在按下回车之前就想好这个项目叫什么、包名前缀是什么、面向哪些平台、模板用哪种。后面我会把每个参数都说清楚。1.2 初始化前的半小时怎样影响后面的维护成本我观察过两个对比明显的项目。A 项目创建时全用默认值开发同学直接在lib/main.dart里堆页面文件按页面平铺功能模块混在一起。一个半月后要加离线缓存发现所有跟网络相关的请求逻辑散落在各处状态管理也换过两轮那段时间基本是在重构旧代码。B 项目创建第一天就按 feature 分组每个功能模块下再分data/domain/presentation子目录路由、依赖注入、状态管理都定了基准方案。后面迭代明显顺畅新同学进来也能快速定位代码。这两个项目差了不是代码量而是“起步时的架构决策”。脚手架只保证“最小可用”但“最小可用”不等于“可扩展”。状态管理选哪家、路由怎么组织、目录按类型还是按功能划分这些是工具不会替你做的决定。新建项目这个动作本质上是把这一类决定集中做一遍。2. 动手前先检查环境flutter doctor 的每一行怎么读在运行flutter create之前我会先跑一遍flutter doctor。大部分人只看最后一句“No issues found”但中间几行如果有警告新项目八成会在第一天踩坑。2.1 用诊断输出反向检查工具链flutter doctor的输出是有明确分组的我建议按下表逐项核对检查项正常情况下应显示需要特别留意的点FlutterFlutter · channel stable · Dart 3.x版本是否被团队其他成员统一Android toolchainAndroid SDK 34 · Java 17JDK 版本是否在 17 及以上Xcode / iOSXcode 15 · CocoaPods installedCocoaPods 是否安装并可正常更新Chrome / WebChrome for web development做 Web 端调试需要IDE 插件VS Code / Android Studio 插件就绪缺少插件会少很多调试便利如果某一行出现[!]不要急着忽略。常见几种情况Android licenses 未接受命令是flutter doctor --android-licenses一路按 y 即可Java 版本不对java -version确认Flutter 3.x 对 Java 17 支持最好不一定非要装最新版iOS 侧报 CocoaPods 问题新项目晚点也会在这里卡住最好在创建之前处理好。2.2 create 参数是决定新生项目“出身”的地方flutter create的参数很多但真正值得每次都敲的全在这里参数作用我的推荐写法--org反向域名前缀直接影响 Android applicationId 和 iOS Bundle IDcom.example先占位后续再统一--project-nameDart 包名必须全小写下划线不能有连字符与仓库名保持一致--platforms只生成需要的平台壳工程先android,ios后面可补--templateapp、package、plugin、skeleton等业务项目选app--empty生成极简 main.dart不附带计数器 Demo我强烈推荐我自己新建 App 项目时命令基本是固定格式flutter create --org com.example --project-name my_app --platforms android,ios --template app --empty my_app--empty这个参数很多人没用过。它生成的lib/main.dart干净到几乎没有样板代码不会给你一屏幕的 Counter Demo。别小看这个差异——默认模板自带的计数器页面和对应测试会在新项目第一天给你一种“先跑通再说”的错觉而--empty直接把起点切成真实项目的样子。2.3 新项目成立后先做三件不起眼的小事创建完成后我的固定顺序是cd my_app flutter analyze flutter test flutter run -d chromeflutter analyze会立刻暴露未使用的变量、类型问题、格式问题flutter test保证测试链路是通的flutter run -d chrome则能不开模拟器就验证“代码能编译成可运行产物”。这三件事加起来不到两分钟却能把后续调试时一半的“怎么跑不起来”消灭在萌芽阶段。3. 默认模板不等于最佳模板新建当天我会改掉的几个默认项flutter create生成的模板质量相当高但它是面向大众的。真实项目通常会基于它做一轮“私人订制”我建议在新建当天就把下面这些改掉不要拖。3.1 计数器 Demo 先放一边用 --empty 起步更干净不是计数器 Demo 有什么问题而是真实项目迟早要把那套占位代码删干净。既然迟早要删不如一开始就不生成。--empty模板生成的 main.dart 只有最小启动代码这能帮你从第一个 commit 开始就把目录规划成“业务项目的形状”而不是“示例改出来的形状”。3.2 pubspec.yaml 先补三行打开pubspec.yaml我至少会改三处name: my_app description: 一个用于新项目初始化的示例工程。 publish_to: none environment: sdk: 3.4.0 4.0.0description看起来很不起眼但它会在 API 文档、错误上报和同事核对依赖时反复出现publish_to: none表示这个工程不是要发布到公共包仓库的包environment.sdk写清约束能防止某天在更低版本 SDK 上跑出莫名奇妙的兼容问题。3.3 最小可用的 main.dart 骨架用--empty创建后我第一件事是把 main.dart 补成下面这个样子import package:flutter/material.dart; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: My App, theme: ThemeData( colorSchemeSeed: Colors.indigo, useMaterial3: true, ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(Home)), body: const Center(child: Text(Hello, Flutter .new)), ); } }这里useMaterial3: true在较新版本里已经是默认值但显式写出来能让团队明确“我们采用的是 Material 3 设计基线”减少后续组件风格不一致的讨论。3.4 analysis_options.yaml还没有代码就先定规矩代码风格这种东西等写了几千行再统一就要靠大范围重构了。新建项目当天就应该把analysis_options.yaml打开确认它默认带着flutter_lints再补几个我常用的规则include: package:flutter_lints/flutter.yaml linter: rules: prefer_const_constructors: true prefer_const_declarations: trueprefer_const_constructors会提醒你给不变的组件加const这个小规则能让渲染性能更稳也让代码意图更清楚。配合 IDE 的“保存时自动格式化”新项目的代码风格偏差会小很多。3.5 平台目录里的隐藏坑默认生成的 App 图标是 Flutter 官方图标应用名也叫项目名这些一般都要改。但我不建议手动去改 Android/iOS 的图标资源比较折腾。我用的是flutter_launcher_icons在dev_dependencies里加一行dev_dependencies: flutter_launcher_icons: ^0.13.1然后把图标路径配置好运行dart run flutter_launcher_icons即可。注意版本号要以包管理器上的最新版本为准我只是给个参考。另外一个很容易漏的点iOS 的启动页面名称、Android 的android:label。这些决定了 App 装到手机上之后显示的文字建议新建当天就统一成正式产品名否则后面容易被内部测试包搞得晕头转向。3.6 测试文件从第一天起就写真正有用的检查默认模板会生成一个针对计数器的widget_test.dart如果你用--empty测试目录基本是空的。别让它空着。我倾向先写一个最简单的冒烟测试确认主页能渲染出来import package:flutter_test/flutter_test.dart; import package:my_app/main.dart; void main() { testWidgets(首页应该显示标题, (tester) async { await tester.pumpWidget(const MyApp()); expect(find.text(Home), findsOneWidget); }); }这种测试看起来简单但它固定了一个关键事实应用的入口组件能正常 mount。以后如果有人在启动流程里塞了会抛异常的初始化逻辑这个测试会第一时间跳出来报错。4. 状态管理与路由选型新项目第一天就该定下来的事新项目最耗精力的不是写页面而是选型。状态管理、路由、依赖注入这三样不趁早定后面每个功能模块都可能各自为政。4.1 状态管理怎么选别按社区热度选按项目规模选我给出的判断框架是先看规模再看团队方案适合规模模板代码量学习成本我的建议setState StatefulWidget工具型、极小页面低低先把这个用熟再做选择Provider中小型 App低中团队还没统一方案时可以起步Riverpod中大型 App中中我个人目前的主力Bloc大型、强规范团队高高模块边界清晰时收益明显选状态管理的核心逻辑不是“哪个火选哪个”而是“哪个能在未来半年内减少沟通成本”。如果你一个人维护小项目setState 完全够用如果是三个人以上协作Riverpod 这种带显式依赖关系的方案会更稳如果模块很多、需要强约束Bloc 的事件驱动模型能帮团队把业务逻辑写成相对统一的形状。4.2 路由自定义 Navigator 还是上框架Flutter 自带的路由能力分成两代。Navigator 1.0用push/pop写起来直观但页面一多就会出现散落各处的跳转逻辑Navigator 2.0提供完整能力但要手动维护路由栈状态容易做错。我建议中大型新项目直接上声明式路由比如 go_router因为它的核心价值不只是“跳转”而是把路由表收拢成一个可测试、可深链的对象final router GoRouter( routes: [ GoRoute( path: /, builder: (context, state) const HomePage(), ), GoRoute( path: /detail/:id, builder: (context, state) DetailPage( id: state.pathParameters[id] ?? , ), ), ], );新建项目在第一天就把MaterialApp.router接好后面每新增一个页面就是在路由表里加一行比翻代码找Navigator.push要容易得多。4.3 我给最常见起步项目的组合建议不要第一天就上全家桶。我常用的演进路径是第一个版本先用setState和原生 Navigator 写跑通核心业务页面超过五六个、共享状态开始出现时引入 Riverpod模块边界真正清晰、团队多人协作时再把 Bloc 的层隔离引入。依赖注入也不一定要上框架。构造函数传依赖在中小项目里完全够用硬上框架反而会让每个页面多一层“从哪拿对象”的成本。新项目第一天要定的不是某个库而是演进路径——比如“我们先用 Riverpod等某条业务线复杂到需要事件流再局部引入 Bloc”这个共识比库本身重要得多。5. 从“能跑”到“稳定跑”新项目必须完成的基础配置很多人跑到flutter run能看到页面就以为新项目完成了。但“能跑”和“能长期稳定地跑”之间还隔着一堆基础工程配置。5.1 应用标识、版本号、图标与启动页版本号在pubspec.yaml里看起来只有一行version: 1.0.01它的含义是版本名 构建号。Android 平台会把这两个值映射到versionName和versionCodeiOS 侧需要在 Xcode 里同步。建议新项目第一天就把版本号策略定下来比如功能迭代升版本名每次打包升构建号。否则在 TestFlight 或内部测试时你根本分不清装的是哪个包。应用图标和启动页也属于基础设施越早定越好。我见过项目上线前一周才换图标结果 iOS 审核因启动画面不符合规范被退回整个团队陪着加班。这些活适合放在项目中前期做越早做越不着急。5.2 热重载的边界哪些改动必须重启flutter run之后大多数人习惯按r热重载但热重载不是万能的。我用下来的经验是改动类型热重载热重启完整重新构建Widget 树的细节调整可以可以可以修改 main() 顶层逻辑不行可以可以修改 pubspec.yaml 依赖不行不行必须重启原生 Android/iOS 代码不行不行必须重新构建遇到“改了代码屏幕没反应”先判断改的是哪一种。尤其是pubspec.yaml改过依赖之后热重载不会生效必须停掉进程重新flutter run。这条规则早一点写进团队约定能省掉很多“明明改了为什么没变化”的困惑。5.3 工程质量基线让 analyze 和 test 跑在每次提交里新项目第一天就可以挂上持续集成。基础工作流很简单拉取代码、装依赖、跑静态检查、跑测试。name: flutter_ci on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: subosito/flutter-actionv2 with: channel: stable - run: flutter pub get - run: flutter analyze - run: flutter test这里我建议把channel显式写成stable甚至锁一个具体 Flutter 版本。因为flutter analyze的规则和警告会随版本变化不锁版本的话团队不同成员可能得到不同的分析结果持续集成的意义就少了一半。5.4 构建产物最小体积从第一天开始考虑移动端包体积的优化不是后期专项而是构建姿势的问题。Android 构建我建议从一开始就用flutter build apk --release --split-per-abi--split-per-abi会让 Gradle 按 CPU 架构分别产出 armeabi-v7a、arm64-v8a、x86_64 三种包整体体积比单一大包小不少。上架应用商店时我建议用flutter build appbundle --release让商店按设备架构分发。iOS 侧的对应命令是flutter build ipa。这些命令写在项目 README 的“构建指南”里后来者就不会凭着记忆乱敲。6. 新项目启动后最容易卡住的四个场景与排查思路准备做得再充分新项目启动阶段还是会遇到莫名其妙的问题。我按踩坑频率排序把四个典型场景的完整排查链路写出来遇到类似问题可以直接照着做。6.1 Gradle 卡在下载依赖或构建超时很多 Flutter 新项目第一次跑 Android 端会长时间停在Running Gradle task assembleDebug...然后超时。这不代表代码有问题而是一次构建在下载大量依赖。我的排查顺序是先看 JDK 版本java -versionFlutter 3.x 建议用 JDK 17版本太老会直接触发 Gradle 兼容错误停掉可能锁住 Gradle 的旧进程cd android ./gradlew --stop回到项目根目录做一次干净构建flutter clean flutter pub get flutter run如果还卡确认本机网络能否稳定连接依赖仓库。这条链路里flutter clean常常被人忽略但它在“Gradle 状态和 pub 缓存不一致”时非常有效。6.2 模拟器上热重载不生效或白屏热重载不生效比构建失败更烦。遇到白屏我建议不要直接反复按r而是做一次链路排查用flutter run -v跑起来看完整日志里有没有PlatformException或 Dart 异常日志看不出问题时按大写R做热重启重新初始化整个 widget 树如果热重启也没用检查模拟器状态有些模拟器的缓存和真机差异很大最后兜底还是flutter clean再跑。这类问题绝大多数不是热重载本身坏了而是 App 的全局状态被污染了。尤其是环境配置、启动初始化逻辑这类代码热重载保留状态的特征反而让问题残留热重启和完整重启才是正规解法。6.3 新建的 iOS 工程在模拟器上编译失败iOS 侧的常见故障基本围绕 Xcode 和 CocoaPods。第一次编译失败时我按这个顺序查确认 Xcode 版本是否满足当前 Flutter 版本要求版本差太大会出现奇怪的编译报错执行cd ios pod install --repo-update很多问题是 Pods 没有生成或依赖版本陈旧如果 Pods 还是报错删除ios/Pods和Podfile.lock重新pod install签名类报错先不慌模拟器构建本来不需要真机签名检查一下 Project 里 Signing 配置是否被误设成真机模式。不要一上来就改 Podfile特别是不要为了消除警告去动platform版本那经常是饮鸩止渴。6.4 “我这里没报错啊”构建环境不一致的灵异问题团队协作时最经典的一幕是A 说项目跑得好好的B 却一编译就崩。大部分情况下是构建环境悄悄不一致了。我建议优先核对这四个变量变量典型症状核对方式Flutter SDK 版本依赖编译报错flutter --versionpubspec.lock依赖版本不一致是否已提交到仓库JDK 版本Gradle 异常java -versionXcode / Android toolchain平台构建失败flutter doctor -v一个非常实用的习惯从新项目创建那天起就把 Flutter 版本、JDK 版本、各平台工具版本写进 README并让 CI 使用同一套版本。出现灵异问题时先flutter clean再重启 IDE比漫无目的地改代码有效得多。最后说点个人体会。我现在每次新建项目第一件事不是打开编辑器写代码而是先在 README 里写清环境版本和目录约定再执行那条flutter create。这个动作看起来多此一举但两个月后新同学加入时它会帮你省掉大量解释成本。Flutter .new 的真正含义对我来说不是一条命令而是把一个项目从“能跑”推向“能长期维护”的整个起点流程。希望这份记录对你的下一个新项目也有帮助。
阅读完成 · 觉得有帮助?