本文以仓库中的 flutter_demo_app为案例,按照真实交付顺序拆成 7 个阶段。每个阶段都有目标、操作、代码骨架、验收标准和常见问题。建议按顺序完成,每阶段都保留一个可运行版本。
1.先认识本项目
当前代码已经具备一个可运行的 UI 原型:
lib/
main.dart # 应用入口和主题
config/ # 颜色、尺寸、文字规范
pages/login/ # 登录页
pages/home/ # 首页、记录、我的
widgets/ # 通用输入框、按钮、卡片
这是"页面优先"的起点。学习过程中逐步补上 app/、data/、domain/ 和 services/,不要一次性重写全部页面。
2.项目搭建:环境配置与软件安装
2.1 推荐环境
- Flutter stable(以
flutter --version输出为准)- Dart SDK 由 Flutter 自带,需满足
pubspec.yaml的 SDK 约束- Android Studio:Android SDK、Platform Tools、模拟器、JDK
- VS Code + Flutter/Dart 插件(或 Android Studio 插件)
- Git、Chrome(调试 Web 可选)
安装后依次执行:
flutter doctor -v
flutter devices
flutter create flutter_demo_app
cd flutter_demo_app
flutter pub get
flutter run
flutter doctor中带!的项先解决;Android 许可可用flutter doctor --android-licenses` 接受。团队应固定 Flutter 版本(FVM、CI 镜像或文档记录),避免不同版本生成不同锁文件。
2.2 初始化与提交纪律
git init
git add .
git commit -m "chore: bootstrap flutter app"
确认 .gitignore 已忽略 .dart_tool/、build/、密钥和本地配置。不要提交 android/key.properties、Keystore 或生产 API 密钥。
阶段验收:新机器能执行 flutter pub get && flutter test,模拟器能看到登录页。
3.工程化与架构熟悉
3.1 从页面目录演进为分层目录
推荐结构:
lib/
app/ # App、路由、主题、环境
config/ # 颜色、尺寸、常量
core/ # 网络、异常、存储、日志
data/
models/ # JSON 模型
datasources/ # API/本地数据源
repositories/ # 仓储实现
domain/ # 业务接口和用例(可选)
pages/ # 页面和页面状态
widgets/ # 跨页面通用组件
页面只负责展示和用户事件;Repository 决定从网络还是缓存取数据;Model 负责 JSON 转换。这样在线、离线和测试都能替换数据源。
3.2 依赖与质量门禁
按需要添加依赖(版本以项目当前稳定版为准):
dependencies:
dio: ^5.0.0 # HTTP、拦截器、超时
go_router: ^14.0.0 # 声明式路由和鉴权重定向
shared_preferences: ^2.0.0 # 轻量配置
flutter_riverpod: ^2.0.0 # 可测试的状态管理
json_annotation: ^4.0.0
dev_dependencies:
build_runner: ^2.0.0
json_serializable: ^6.0.0
不要为了"完整架构"盲目引入包:只有当登录状态、列表分页或缓存开始复杂时再引入状态管理和生成器。
每次提交前执行:
dart format lib test
flutter analyze
flutter test
4.全局配置与封装
4.1 主题、颜色、间距
仓库已有 ColorConfig、SizeConfig、TextConfig,统一在 ThemeData 汇总,页面不直接散落颜色值:
javascript
ThemeData buildTheme() => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: ColorConfig.primary),
scaffoldBackgroundColor: ColorConfig.page,
inputDecorationTheme: const InputDecorationTheme(isDense: true),
);
常量建议集中为 AppConstants(接口超时、分页大小、缓存 key、产品名),环境差异放 AppConfig:
javascript
class AppConfig {
static const environment = String.fromEnvironment('APP_ENV', defaultValue: 'dev');
static const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.example.com');
}
运行时注入:flutter run --dart-define=APP_ENV=dev --dart-define=API_BASE_URL=https://dev-api.example.com。
4.2 网络、异常和存储封装
Dio统一配置 baseUrl、超时、token 拦截器和错误映射;Repository 不直接操作Dio。本地存储只保存 token、记住的账号和离线队列,敏感 token 生产环境改用 flutter_secure_storage。
javascript
class ApiClient {
ApiClient(String baseUrl) : dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
));
final Dio dio;
}
所有错误转成可显示的业务错误(网络不可用、登录失败、服务端错误),页面只处理结果,不解析 HTTP 状态码。
阶段验收:修改主题色只改一处;开发/测试 API 可通过 --dart-define 切换;网络异常能显示明确提示。
5. 页面交互开发:登录、首页、表单、路由、弹窗和状态
5.1 路由与登录态
用 go_router 集中定义 /login、/home、/records,并用 refreshListenable 或 provider 监听 token。未登录访问业务页重定向到登录页,退出登录清除 token 后回到 /login。不要在每个按钮里手写嵌套 MaterialPageRoute。
5.2 登录表单
将当前 TextField 升级为 Form + GlobalKey<FormState>:账号必填且长度合法,密码至少 6 位;提交时先 validate(),再将按钮置为 loading,防止重复点击。密码框增加显示/隐藏图标,错误使用 InputDecoration.errorText,成功后保存 token 和记住账号。
javascript
final formKey = GlobalKey<FormState>();
if (!(formKey.currentState?.validate() ?? false)) return;
setState(() => loading = true);
try {
final session = await authRepository.login(account.text.trim(), password.text);
if (!mounted) return;
ref.read(sessionProvider.notifier).setSession(session);
} on AppException catch (e) {
if (mounted) showAppError(context, e.message);
} finally {
if (mounted) setState(() => loading = false);
}
5.3 首页状态与交互
首页拆成"用户信息、实验室选择、快捷操作、记录列表、我的"组件。列表必须具备 loading、空数据、错误、下拉刷新和分页状态;实验室选择用单一状态保存当前 id,而不是只弹 Toast。删除、退出登录等破坏性操作使用 showDialog 二次确认;轻量反馈使用 SnackBar。
5.4 响应式和无障碍
以 LayoutBuilder/MediaQuery 适配手机和大屏;避免固定高度包裹可变文本。按钮提供语义化文本,颜色不能作为唯一状态提示,输入控件设置键盘类型和 textInputAction。
阶段验收:空密码不能提交;提交期间按钮不可重复点击;登录成功可返回首页,退出后不能用返回键回到业务页;首页可刷新并正确显示空/错/加载状态。
6. 数据交互:前后端接口与在线/离线模式
6.1 定义模型和接口
以 LoginRequest、Session、InspectionRecord 为例,使用 json_serializable 生成 fromJson/toJson。Repository 接口表达业务意图:login、listRecords、saveInspection,而不是暴露 Dio。
javascript
abstract interface class RecordRepository {
Future<List<InspectionRecord>> list({int page = 1});
Future<void> save(InspectionRecord record);
}
为每个接口写请求参数、响应示例、鉴权要求、错误码和超时策略;后端字段变更时只改 model/data 层。
6.2 在线优先与离线队列
读取采用 cache-first 或 network-first 策略并明确 UI 标识"离线数据";写入在线直接提交,网络失败则写入本地队列,带 pending/syncing/synced/failed 状态、重试次数和幂等 requestId。应用启动和网络恢复时执行同步,冲突按服务端版本号或最后修改时间处理,不能静默覆盖。
javascript
Future<List<InspectionRecord>> listRecords() async {
try {
final remote = await api.listRecords();
await cache.replace(remote);
return remote;
} on DioException {
return cache.read();
}
}
测试重点:断网仍能查看最近缓存;重复点击不会产生两条记录;同步失败可重试且用户能看到原因;token 过期会清队列或要求重新登录(按业务策略)。
7. 打包部署与性能优化
7.1 Android 签名
生成并保护 Keystore(路径示例仅用于本机):
keytool -genkeypair -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
在 android/key.properties 配置(该文件已经加入 .gitignore):
storePassword=替换为实际密码
keyPassword=替换为实际密码
keyAlias=upload
storeFile=/绝对路径/upload-keystore.jks
在 android/app/build.gradle.kts 顶部读取配置,并让 release 构建引用签名:
javascript
import java.util.Properties
val keystoreProperties = Properties().apply {
val file = rootProject.file("key.properties")
if (file.exists()) file.inputStream().use(::load)
}
android {
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release { signingConfig = signingConfigs.getByName("release") }
}
}
CI 使用 Secret 在构建时生成配置,绝不把密码或 Keystore 提交到仓库。正式发布前使用 apksigner verify --verbose app-release.apk 检查签名,并把 Keystore 加密备份;丢失上传密钥会显著增加后续发版成本。
7.2 版本、构建和产物
pubspec.yaml 的 version: 1.2.0+15 分别对应 versionName/versionCode。构建命令:
flutter clean && flutter pub get
flutter build apk --release --split-per-abi --build-name=1.2.0 --build-number=15
flutter build appbundle --release
优先发布 AAB 到 Google Play;内部分发可用 APK。构建前检查图标、包名、隐私声明、网络安全配置和崩溃上报。
7.3 性能清单
- 用
const、ListView.builder,避免大列表一次性List.generate。- 图片指定尺寸并缓存,避免在
build中做 JSON/排序/网络请求。- 用 Flutter DevTools 的 Performance、Memory、Network 面板检查首帧、卡顿和泄漏。
- release 包关闭 debug 日志;按 ABI 拆包并压缩资源。
- 以真实低端设备测量 60fps、启动耗时、内存峰值,而不是只看模拟器。
阶段验收:release APK 可安装、签名校验通过、版本号正确;关键页面滚动无明显掉帧;线上 API 地址由构建参数注入。
8. 常用命令速查
flutter channel stable && flutter upgrade
切换Flutter到stable稳定分支,并升级SDK到当前分支最新版本
flutter doctor -v
Flutter环境全面体检,-v输出详细信息,排查SDK、Android、Xcode等环境问题
flutter pub get
根据pubspec.yaml下载/同步项目全部第三方依赖包
flutter pub add dio
快速添加第三方依赖库dio(网络请求库),自动写入配置并执行
pub get flutter pub outdated
检测项目所有依赖,查看哪些包存在可升级新版本
dart format .
格式化当前项目全部Dart代码,统一官方编码风格
flutter analyze
静态代码检查,不运行程序,扫描代码警告、潜在错误、语法问题
flutter test
执行test目录下全部单元测试用例,无需真机/模拟器
flutter test --coverage
执行单元测试,同时生成代码测试覆盖率报告
flutter run -d <device>
指定设备编译并运行项目,<device>填写设备名称/序列号,支持热重载调试
flutter logs
查看连接设备上App运行日志(print打印输出)
flutter clean
清理编译缓存,删除build、.dart_tool等产物,解决编译异常时使用
flutter build apk --release
构建Android正式发布版APK安装包,产物可直接给安卓手机安装
flutter build appbundle --release
构建AAB包,用于应用商店上架,不能直接手机安装
adb devices
adb命令:列出所有已连接的安卓真机/模拟器设备
adb install -r build/app/outputs/flutter-apk/app-release.apk
adb命令:覆盖安装release版本apk到设备,-r保留应用原有数据
遇到"依赖/生成文件异常"先 flutter clean && flutter pub get;遇到布局问题用 DevTools Inspector;遇到 Android 构建问题先核对 JDK、Gradle、compileSdk 与 Flutter 版本矩阵。
9.推荐学习节奏与交付物
按 3 周、每天 1.5~2 小时推进:
- 第 1~2 天:环境、Git、Flutter/Dart 基础,交付可运行空壳。
- 第 3~5 天:目录、主题、通用组件、静态登录/首页,交付 UI 原型。
- 第 6~9 天:路由、表单校验、状态管理、弹窗和测试,交付可操作 Demo。
- 第 10~13 天:API、模型、Repository、错误处理,交付在线版。
- 第 14~16 天:缓存、离线队列、同步和断网测试,交付离线可用版。
- 第 17~19 天:签名、CI 构建、DevTools 性能优化,交付 release APK/AAB。
- 第 20~21 天:补文档、回归测试、版本发布清单,完成一次模拟上线。
最终验收应包括:新环境搭建文档、架构图、接口文档、测试报告、签名构建说明、APK/AAB 产物和回滚方案。完成这套闭环后,再学习动画、国际化、推送、后台任务等专项能力,收益会更高。