first commit
This commit is contained in:
530
NOTIFICATION_IMPLEMENTATION.md
Normal file
530
NOTIFICATION_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# 📱 本地通知功能 - 实现文档
|
||||
|
||||
**实现日期**: 2025-11-22
|
||||
**状态**: ✅ 完成
|
||||
**平台支持**: Android, iOS (Web不支持)
|
||||
|
||||
---
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
FocusBuddy 现在支持本地通知,在专注计时完成时自动提醒用户,即使应用在后台运行。
|
||||
|
||||
### 核心特性
|
||||
- ✅ 计时完成时自动发送通知
|
||||
- ✅ 显示专注时长和分心次数
|
||||
- ✅ 支持震动和声音
|
||||
- ✅ 自动请求权限
|
||||
- ✅ Web 平台优雅降级(不报错)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
lib/services/notification_service.dart # 通知服务(新增)
|
||||
lib/main.dart # 初始化通知服务(已修改)
|
||||
lib/screens/focus_screen.dart # 计时完成时调用通知(已修改)
|
||||
android/app/src/main/AndroidManifest.xml # Android权限(已修改)
|
||||
```
|
||||
|
||||
### 服务设计
|
||||
- **单例模式**: 全局只有一个 NotificationService 实例
|
||||
- **延迟初始化**: 首次调用时才初始化
|
||||
- **平台检测**: 自动识别 Web 平台并跳过
|
||||
|
||||
---
|
||||
|
||||
## 📝 代码实现
|
||||
|
||||
### 1. NotificationService 类
|
||||
|
||||
**位置**: `lib/services/notification_service.dart`
|
||||
|
||||
**关键方法**:
|
||||
|
||||
#### `initialize()` - 初始化服务
|
||||
```dart
|
||||
Future<void> initialize() async {
|
||||
if (_initialized) return;
|
||||
|
||||
// Web 平台跳过
|
||||
if (kIsWeb) return;
|
||||
|
||||
// Android/iOS 配置
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(...);
|
||||
|
||||
await _notifications.initialize(initSettings);
|
||||
}
|
||||
```
|
||||
|
||||
#### `requestPermissions()` - 请求权限(iOS)
|
||||
```dart
|
||||
Future<bool> requestPermissions() async {
|
||||
// iOS 需要显式请求,Android 自动授予
|
||||
final result = await _notifications
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(...);
|
||||
return result ?? true;
|
||||
}
|
||||
```
|
||||
|
||||
#### `showFocusCompletedNotification()` - 显示完成通知
|
||||
```dart
|
||||
Future<void> showFocusCompletedNotification({
|
||||
required int minutes,
|
||||
required int distractionCount,
|
||||
}) async {
|
||||
final title = '🎉 Focus session complete!';
|
||||
final body = distractionCount == 0
|
||||
? 'You focused for $minutes minutes without distractions!'
|
||||
: 'You focused for $minutes minutes. Great effort!';
|
||||
|
||||
await _notifications.show(0, title, body, details);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. main.dart 初始化
|
||||
|
||||
**修改内容**:
|
||||
```dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await StorageService.init();
|
||||
|
||||
// 新增:初始化通知服务
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await notificationService.requestPermissions();
|
||||
|
||||
runApp(MyApp(...));
|
||||
}
|
||||
```
|
||||
|
||||
**何时调用**:
|
||||
- 应用启动时自动初始化
|
||||
- 自动请求权限(iOS 会弹出权限对话框)
|
||||
|
||||
---
|
||||
|
||||
### 3. FocusScreen 计时完成
|
||||
|
||||
**修改位置**: `lib/screens/focus_screen.dart` 第 56-79 行
|
||||
|
||||
**修改内容**:
|
||||
```dart
|
||||
void _onTimerComplete() async {
|
||||
_timer.cancel();
|
||||
_saveFocusSession(completed: true);
|
||||
|
||||
// 新增:发送通知
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.showFocusCompletedNotification(
|
||||
minutes: widget.durationMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(...);
|
||||
}
|
||||
```
|
||||
|
||||
**触发时机**:
|
||||
- 计时器倒数到 0 时
|
||||
- 在导航到 Complete Screen 之前
|
||||
- 保存数据之后
|
||||
|
||||
---
|
||||
|
||||
## 📱 Android 配置
|
||||
|
||||
### AndroidManifest.xml
|
||||
|
||||
**文件位置**: `android/app/src/main/AndroidManifest.xml`
|
||||
|
||||
**添加的权限**:
|
||||
```xml
|
||||
<!-- Android 13+ 通知权限(必需)-->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- 震动权限(可选,增强体验)-->
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<!-- Wake Lock(可选,后台计时)-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
```
|
||||
|
||||
### 权限说明
|
||||
|
||||
| 权限 | 必需性 | 用途 | Android版本 |
|
||||
|------|--------|------|------------|
|
||||
| POST_NOTIFICATIONS | **必需** | 发送通知 | 13+ (API 33+) |
|
||||
| VIBRATE | 可选 | 通知震动 | 所有版本 |
|
||||
| WAKE_LOCK | 可选 | 后台计时 | 所有版本 |
|
||||
|
||||
### Android 权限流程
|
||||
|
||||
**Android 12 及以下**:
|
||||
1. 自动授予通知权限
|
||||
2. 无需用户确认
|
||||
|
||||
**Android 13+**:
|
||||
1. 首次发送通知时,系统自动弹出权限对话框
|
||||
2. 用户可以选择"允许"或"拒绝"
|
||||
3. 拒绝后可在设置中手动开启
|
||||
|
||||
---
|
||||
|
||||
## 🍎 iOS 配置
|
||||
|
||||
### Info.plist
|
||||
|
||||
**不需要修改** - iOS 通知权限在运行时请求,无需配置文件声明。
|
||||
|
||||
### iOS 权限流程
|
||||
|
||||
1. **首次启动**:
|
||||
- App 启动时调用 `requestPermissions()`
|
||||
- 系统弹出权限对话框:
|
||||
```
|
||||
"FocusBuddy" Would Like to Send You Notifications
|
||||
Notifications may include alerts, sounds, and icon badges.
|
||||
[Don't Allow] [Allow]
|
||||
```
|
||||
|
||||
2. **用户选择**:
|
||||
- **允许**: 正常发送通知
|
||||
- **拒绝**: 静默失败(不影响应用运行)
|
||||
|
||||
3. **后续修改**:
|
||||
- 用户可在 设置 > FocusBuddy > 通知 中修改
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Web 平台处理
|
||||
|
||||
### 策略:优雅降级
|
||||
|
||||
**为什么 Web 不支持**:
|
||||
- `flutter_local_notifications` 不支持 Web
|
||||
- Web 使用不同的通知 API(需要 Service Worker)
|
||||
|
||||
**如何处理**:
|
||||
```dart
|
||||
if (kIsWeb) {
|
||||
print('Notifications not supported on web platform');
|
||||
return; // 静默跳过,不报错
|
||||
}
|
||||
```
|
||||
|
||||
**用户体验**:
|
||||
- Web 版用户不会看到通知
|
||||
- 不会报错或崩溃
|
||||
- 计时完成后仍正常跳转到 Complete Screen
|
||||
|
||||
**未来改进** (可选):
|
||||
- 使用 Web Notification API
|
||||
- 或显示应用内弹窗提示
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试指南
|
||||
|
||||
### Android 测试
|
||||
|
||||
#### 准备工作
|
||||
```bash
|
||||
# 1. 连接 Android 设备或启动模拟器
|
||||
flutter devices
|
||||
|
||||
# 2. 运行应用
|
||||
flutter run -d <android-device-id>
|
||||
```
|
||||
|
||||
#### 测试步骤
|
||||
|
||||
**测试 1: 首次权限请求(Android 13+)**
|
||||
```
|
||||
1. 卸载应用(清除权限状态)
|
||||
2. 重新安装并启动
|
||||
3. 开始一次专注(设置为 1 分钟测试)
|
||||
4. 等待计时完成
|
||||
5. 预期:系统弹出权限对话框
|
||||
6. 点击"允许"
|
||||
7. 预期:看到通知
|
||||
```
|
||||
|
||||
**测试 2: 前台通知**
|
||||
```
|
||||
1. 应用在前台
|
||||
2. 开始专注(1分钟)
|
||||
3. 等待完成
|
||||
4. 预期:
|
||||
- 顶部通知栏出现通知
|
||||
- 有震动(如果手机未静音)
|
||||
- 有声音(如果手机未静音)
|
||||
- 自动跳转到 Complete Screen
|
||||
```
|
||||
|
||||
**测试 3: 后台通知**
|
||||
```
|
||||
1. 开始专注(1分钟)
|
||||
2. 按 Home 键,应用进入后台
|
||||
3. 等待计时完成
|
||||
4. 预期:
|
||||
- 收到通知
|
||||
- 点击通知可回到应用
|
||||
- 看到 Complete Screen
|
||||
```
|
||||
|
||||
**测试 4: 拒绝权限**
|
||||
```
|
||||
1. 在设置中禁用通知权限
|
||||
2. 开始专注
|
||||
3. 完成后不会有通知
|
||||
4. 但应用正常跳转到 Complete Screen
|
||||
5. 预期:无崩溃
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### iOS 测试
|
||||
|
||||
#### 准备工作
|
||||
```bash
|
||||
# 需要 macOS + Xcode
|
||||
# 1. 连接 iPhone 或启动模拟器
|
||||
flutter devices
|
||||
|
||||
# 2. 运行应用
|
||||
flutter run -d <ios-device-id>
|
||||
```
|
||||
|
||||
#### 测试步骤
|
||||
|
||||
**测试 1: 权限对话框**
|
||||
```
|
||||
1. 首次启动应用
|
||||
2. 预期:立即看到权限对话框
|
||||
"FocusBuddy Would Like to Send You Notifications"
|
||||
3. 点击 "Allow"
|
||||
```
|
||||
|
||||
**测试 2: 通知内容**
|
||||
```
|
||||
1. 完成一次专注(0次分心)
|
||||
2. 预期通知内容:
|
||||
标题: 🎉 Focus session complete!
|
||||
正文: You focused for 15 minutes without distractions!
|
||||
|
||||
3. 完成一次专注(3次分心)
|
||||
4. 预期通知内容:
|
||||
标题: 🎉 Focus session complete!
|
||||
正文: You focused for 15 minutes. Great effort!
|
||||
```
|
||||
|
||||
**测试 3: 后台通知**
|
||||
```
|
||||
1. 开始专注
|
||||
2. 滑回主屏幕
|
||||
3. 等待完成
|
||||
4. 预期:锁屏/顶部有通知
|
||||
5. 点击通知打开应用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Web 测试
|
||||
|
||||
#### 测试步骤
|
||||
```
|
||||
1. 运行 Web 版:flutter run -d edge
|
||||
2. 开始专注
|
||||
3. 等待完成
|
||||
4. 预期:
|
||||
- 控制台输出:Notifications not supported on web platform
|
||||
- 无通知
|
||||
- 正常跳转到 Complete Screen
|
||||
- 无报错
|
||||
```
|
||||
|
||||
✅ **通过标准**: 应用正常运行,无崩溃
|
||||
|
||||
---
|
||||
|
||||
## 📊 通知内容逻辑
|
||||
|
||||
### 标题
|
||||
固定内容:`🎉 Focus session complete!`
|
||||
|
||||
### 正文
|
||||
动态内容,根据分心次数变化:
|
||||
|
||||
```dart
|
||||
if (distractionCount == 0) {
|
||||
body = 'You focused for $minutes minutes without distractions!';
|
||||
} else {
|
||||
body = 'You focused for $minutes minutes. Great effort!';
|
||||
}
|
||||
```
|
||||
|
||||
**示例**:
|
||||
- 25分钟,0次分心: "You focused for 25 minutes without distractions!"
|
||||
- 15分钟,3次分心: "You focused for 15 minutes. Great effort!"
|
||||
|
||||
### 设计理念
|
||||
- **正向鼓励**: 即使有分心,也用"Great effort"
|
||||
- **无惩罚**: 不会说"但你分心了3次"
|
||||
- **符合产品价值观**: 温柔、支持、无评判
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置选项
|
||||
|
||||
### 当前实现
|
||||
|
||||
| 特性 | Android | iOS |
|
||||
|------|---------|-----|
|
||||
| 声音 | ✅ | ✅ |
|
||||
| 震动 | ✅ | ✅ |
|
||||
| Badge | ❌ | ✅ |
|
||||
| 优先级 | High | Default |
|
||||
| Channel | focus_completed | - |
|
||||
|
||||
### 可调整的参数
|
||||
|
||||
在 `notification_service.dart` 中:
|
||||
|
||||
```dart
|
||||
// Android 配置
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'focus_completed', // Channel ID(不建议改)
|
||||
'Focus Session Completed', // Channel Name(用户可见)
|
||||
channelDescription: '...', // Channel描述
|
||||
importance: Importance.high, // 重要性(改为 max 会横幅提示)
|
||||
priority: Priority.high, // 优先级
|
||||
enableVibration: true, // 震动开关
|
||||
playSound: true, // 声音开关
|
||||
);
|
||||
|
||||
// iOS 配置
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true, // 显示提示
|
||||
presentBadge: true, // 显示角标
|
||||
presentSound: true, // 播放声音
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来改进建议
|
||||
|
||||
### 优先级低(可选)
|
||||
|
||||
1. **自定义通知声音**
|
||||
- 添加温柔的提示音
|
||||
- 替换系统默认声音
|
||||
- 需要音频资源
|
||||
|
||||
2. **定时提醒**
|
||||
- "你已经2小时没专注了,要来一次吗?"
|
||||
- 使用 `showReminderNotification()`
|
||||
- 需要后台任务(WorkManager)
|
||||
|
||||
3. **通知操作按钮**
|
||||
- Android: 通知上添加 "再来一次" 按钮
|
||||
- 点击直接开始新的专注
|
||||
- 需要额外配置
|
||||
|
||||
4. **通知统计**
|
||||
- "本周你已经专注了 12 小时!"
|
||||
- 定期(如周日)发送总结
|
||||
- 需要调度逻辑
|
||||
|
||||
5. **Web 通知支持**
|
||||
- 使用 Web Notification API
|
||||
- 需要 Service Worker
|
||||
- 需要用户手动授权
|
||||
|
||||
---
|
||||
|
||||
## ❗ 常见问题
|
||||
|
||||
### Q1: 为什么 Android 13 没有弹出权限对话框?
|
||||
**A**: Android 13+ 权限会在**首次发送通知时**自动弹出,不是应用启动时。
|
||||
|
||||
**解决方案**:
|
||||
- 完成一次完整的专注会话
|
||||
- 或在设置中手动开启
|
||||
|
||||
---
|
||||
|
||||
### Q2: iOS 模拟器收不到通知?
|
||||
**A**: iOS 模拟器通知功能有限制。
|
||||
|
||||
**解决方案**:
|
||||
- 使用真机测试
|
||||
- 或检查模拟器的通知设置
|
||||
|
||||
---
|
||||
|
||||
### Q3: Web 版为什么没有通知?
|
||||
**A**: `flutter_local_notifications` 不支持 Web。
|
||||
|
||||
**当前方案**: 优雅降级,不报错
|
||||
**未来方案**: 实现 Web Notification API
|
||||
|
||||
---
|
||||
|
||||
### Q4: 通知没有声音?
|
||||
**A**: 检查以下设置:
|
||||
1. 手机是否静音/勿扰模式
|
||||
2. 应用通知权限是否开启
|
||||
3. 通知重要性是否足够高
|
||||
|
||||
---
|
||||
|
||||
### Q5: 后台计时不准确?
|
||||
**A**: Android 后台限制可能影响计时。
|
||||
|
||||
**建议**:
|
||||
- 添加前台服务(Foreground Service)
|
||||
- 或使用 WorkManager
|
||||
- 当前 MVP 不实现,用户应在前台使用
|
||||
|
||||
---
|
||||
|
||||
## 📝 总结
|
||||
|
||||
### ✅ 已实现
|
||||
- [x] 通知服务架构
|
||||
- [x] 计时完成通知
|
||||
- [x] Android 权限配置
|
||||
- [x] iOS 权限请求
|
||||
- [x] Web 平台兼容
|
||||
- [x] 完整文档
|
||||
|
||||
### 📊 影响
|
||||
- **代码变更**: 3 个文件(新增1个,修改2个)
|
||||
- **新增行数**: ~200 行
|
||||
- **配置变更**: Android + iOS 权限
|
||||
- **测试时间**: ~15 分钟(手动测试)
|
||||
|
||||
### 🎯 MVP 完成度
|
||||
```
|
||||
██████████████████▓░ 95% → 98%
|
||||
```
|
||||
|
||||
**新增功能**: 本地通知 ✅
|
||||
**待完成**: 应用图标、截图、上架准备
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-11-22
|
||||
**维护者**: Claude
|
||||
Reference in New Issue
Block a user