Flutter 项目开发经验积累¶
开发过程中积累的经验教训,可直接复用于其他项目 更新:2026-05-12
一、健康检测机制(启动时自动检测)¶
核心理念¶
项目启动后,自动检测各项功能是否可用,精确定位问题。
App 启动
↓
[健康检测] 逐一检测各项功能
↓
检测结果全部打印日志
↓
有问题 → 友好提示 + 降级方案 → 不闪退
完整健康检测清单¶
| 功能 | 检测方式 | 异常兜底 |
|---|---|---|
| 音频播放 | 播放1秒测试音(最小音量) | 提示"播放器暂不可用" |
| 数据库 | 读写测试 | 切换内存模式 |
| 文件读写 | 读写测试文件 | 提示"文件功能异常" |
| 存储权限 | permission_handler 检查 | 提示授与权限 |
| 网络状态 | connectivity_plus 检测 | 切换离线模式 |
| 通知栏权限 | 检查通知权限 | 提示开启通知 |
| 后台播放权限 | 检查后台音频权限 | 提示开启后台播放 |
| 音频焦点 | 检查音频焦点 | 自动处理焦点抢占 |
| 耳机检测 | 检测耳机插拔状态 | 插拔时自动暂停 |
| 磁盘空间 | 检查剩余空间 | 提示空间不足,停止缓存 |
| OSS连接 | ping OSS endpoint | 离线模式,暂停同步 |
| 内存状态 | 检查剩余内存 | 低内存时清理缓存 |
健康检测日志示例¶
[Vexfy] ===== App 启动 =====
[Vexfy] [健康检测] 数据库: ✅ 正常
[Vexfy] [健康检测] 文件读写: ✅ 正常
[Vexfy] [健康检测] 存储权限: ✅ 已授权
[Vexfy] [健康检测] 网络状态: ✅ 在线
[Vexfy] [健康检测] 通知栏权限: ⚠️ 未授权,请开启以接收后台播放控制
[Vexfy] [健康检测] 音频播放: ✅ 正常
[Vexfy] [健康检测] 后台播放: ✅ 正常
[Vexfy] [健康检测] 磁盘空间: ✅ 剩余 2.3GB
[Vexfy] [健康检测] 内存状态: ✅ 剩余 1.2GB
[Vexfy] ===== App 启动完成 =====
注意:健康检测失败不影响 App 启动,只是提示用户或降级处理。
- 每个检测都要 try-catch,失败不影响 App 启动
- 检测结果全部打印日志,精确到哪个功能有问题
- 日志格式统一:
[健康检测] 功能名: 状态 + 详情 - 任何功能检测失败,要有友好提示而不是闪退
代码示例¶
class HealthChecker {
final Talker logger;
/// 检测播放器
Future<void> checkAudioPlayer() async {
try {
await playerService.playTestSound(); // 播放测试音
logger.info('[健康检测] 音频播放: 正常');
} catch (e, s) {
logger.error('[健康检测] 音频播放: 失败', e, s);
// 降级方案:不崩,显示提示
showToast('播放器暂不可用');
}
}
/// 检测数据库
Future<void> checkDatabase() async {
try {
await db.write('test_key', 'test_value');
final value = await db.read('test_key');
if (value != 'test_value') throw Exception('数据不一致');
logger.info('[健康检测] 数据库: 正常');
} catch (e, s) {
logger.error('[健康检测] 数据库: 失败', e, s);
// 降级方案:切换内存模式
useInMemoryMode();
}
}
/// 检测文件读写
Future<void> checkFileSystem() async {
try {
final file = File('${tempDir}/test_health.txt');
await file.writeAsString('test');
final content = await file.readAsString();
if (content != 'test') throw Exception('读写内容不一致');
await file.delete();
logger.info('[健康检测] 文件读写: 正常');
} catch (e, s) {
logger.error('[健康检测] 文件读写: 失败', e, s);
showToast('文件读写功能异常');
}
}
}
二、日志规范(保证只看日志可精确定位问题)¶
核心原则¶
所有关键步骤都要有日志,关键逻辑要打印入参出参。
日志分级¶
| 级别 | 标签 | 使用场景 |
|---|---|---|
| Debug | [DEBUG] |
调试信息,开发时看 |
| Info | [INFO] |
正常流程,如函数入口 |
| Warning | [WARN] |
异常但可处理,如权限缺失 |
| Error | [ERROR] |
异常且未处理,需要修复 |
日志格式¶
// 格式:[级别] 模块名 消息
// 示例:
logger.info('[PlayerService] 播放歌曲: ${song.title}');
logger.debug('[PlayerService] 入参: filePath=$filePath');
logger.warn('[PlayerService] 文件不存在,降级为在线URL');
logger.error('[PlayerService] 播放失败', e, s);
必须打印日志的地方¶
- 函数入口:打印入参
- 函数出口:打印出参(重要返回值)
- 分支判断:打印条件判断的输入
- 异常捕获:打印异常信息 + 堆栈
- 外部调用:网络请求、数据库操作等
三、异常处理原则(不让任何异常裸奔)¶
核心原则¶
每个 async 函数都要有 try-catch,异常兜底不能少。
处理策略¶
| 场景 | 处理方式 |
|---|---|
| 播放器初始化失败 | 显示"播放器暂不可用",不闪退 |
| 数据库初始化失败 | 切换内存模式兜底 |
| 权限获取失败 | 提示用户,而不是崩溃 |
| 网络请求失败 | 降级到离线模式 |
| 文件读取失败 | 返回空数据,不抛异常 |
代码模板¶
Future<Something> fetchSomething() async {
try {
logger.debug('[Service] fetchSomething 入参: id=$id');
final result = await _repository.fetch(id);
logger.info('[Service] fetchSomething 出参: ${result.length}条');
return result;
} catch (e, s) {
logger.error('[Service] fetchSomething 异常', e, s);
// 降级方案:返回空数据
return [];
}
}
异常兜底原则¶
- 最外层兜底:main() 有 try-catch,App 不闪退
- 模块级兜底:每个 Provider/Service 有 try-catch
- 功能级兜底:关键函数有 try-catch
- 降级方案:失败时提供 fallback,不影响整体功能
四、Flutter Android 打包经验¶
核心规则¶
| 经验 | 说明 |
|---|---|
| jcenter 禁用 | 只用 google() + mavenCentral() |
| on_audio_query 用 3.x | 不要用 1.x,会报 namespace |
| AGP 固定 7.0.4 | 当前最稳定版本 |
| Kotlin 固定 1.8.10 | 配合 AGP 7.0.4 |
| 打包前必 clean | 清除缓存避免异常 |
Gradle Wrapper 反人类设计¶
核心:系统装的 Gradle = 摆设,wrapper 强制自己下载。
最后更新:2026-05-12