Flutter for OpenHarmony轻量级开源记事本app实战:通知设置
本文介绍了Flutter通知设置功能的实现方案。通过NotificationService类管理通知状态,采用GetX实现响应式编程,结合SharedPreferences持久化用户偏好。页面布局分为主开关、基本设置、提醒时间、分类设置和高级设置五个模块,使用Card组件增强视觉层次。技术实现上整合了权限管理、本地存储和屏幕适配等核心功能,提供了清晰易用的通知偏好配置界面,确保用户能够灵活控制应用
设计理念
通知设置是应用管理用户通知偏好的重要功能,它让用户能够控制应用的通知行为、提醒时间和通知方式。一个好的通知设置系统应该提供清晰的选项分类、实时预览效果和智能的通知管理。本文将详细介绍如何实现一个功能完整的通知设置系统。
通知服务的依赖导入
首先导入通知设置所需的核心依赖包。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:permission_handler/permission_handler.dart';
这里导入了五个关键依赖包:Material提供UI组件,GetX实现状态管理和依赖注入,ScreenUtil处理屏幕适配,SharedPreferences负责本地数据持久化,PermissionHandler管理系统权限。这些依赖共同构成了通知设置的技术基础,确保功能的完整性和跨平台兼容性。
通知服务类定义
定义NotificationService类来管理所有通知相关的状态和逻辑。
class NotificationService extends GetxController {
var isNotificationEnabled = true.obs;
var isSoundEnabled = true.obs;
var isVibrationEnabled = true.obs;
var isLedEnabled = false.obs;
var reminderTime = TimeOfDay(hour: 9, minute: 0).obs;
var notificationCategories = <String, bool>{}.obs;
NotificationService继承自GetxController,利用GetX的响应式编程能力。所有状态变量都使用.obs修饰符,使其成为可观察对象,当值变化时自动触发UI更新。reminderTime默认设置为早上9点,notificationCategories使用Map存储不同分类的通知开关状态。这种设计使得状态管理更加简洁,避免了手动调用setState的繁琐操作。
服务初始化方法
实现服务的初始化逻辑,加载保存的设置并检查权限。
Future<void> initialize() async {
await _loadNotificationSettings();
await _checkNotificationPermissions();
}
Future<void> updateSetting(String key, bool value) async {
notificationCategories[key] = value;
await _saveNotificationSettings();
}
initialize方法是服务启动的入口点,按顺序执行设置加载和权限检查两个异步操作。使用await确保操作按序完成,避免竞态条件。updateSetting方法提供了统一的设置更新接口,接收键值对参数,更新后立即持久化到本地存储。这种封装使得外部调用更加简洁,同时保证了数据一致性。
加载通知设置
从本地存储中读取用户的通知偏好设置。
Future<void> _loadNotificationSettings() async {
final prefs = await SharedPreferences.getInstance();
isNotificationEnabled.value = prefs.getBool('notification_enabled') ?? true;
isSoundEnabled.value = prefs.getBool('sound_enabled') ?? true;
isVibrationEnabled.value = prefs.getBool('vibration_enabled') ?? true;
isLedEnabled.value = prefs.getBool('led_enabled') ?? false;
final reminderHour = prefs.getInt('reminder_hour') ?? 9;
final reminderMinute = prefs.getInt('reminder_minute') ?? 0;
reminderTime.value = TimeOfDay(hour: reminderHour, minute: reminderMinute);
}
_loadNotificationSettings方法从SharedPreferences读取所有通知相关配置。使用??运算符提供默认值,确保首次启动时有合理的初始状态。时间设置分别存储小时和分钟,读取后重新构造TimeOfDay对象。这种设计考虑了数据的可序列化性,因为TimeOfDay对象本身无法直接存储,需要拆分为基本类型。默认值的选择也体现了用户友好的设计理念,如LED默认关闭以节省电量。
保存通知设置
将当前的通知设置持久化到本地存储。
Future<void> _saveNotificationSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('notification_enabled', isNotificationEnabled.value);
await prefs.setBool('sound_enabled', isSoundEnabled.value);
await prefs.setBool('vibration_enabled', isVibrationEnabled.value);
await prefs.setBool('led_enabled', isLedEnabled.value);
await prefs.setInt('reminder_hour', reminderTime.value.hour);
await prefs.setInt('reminder_minute', reminderTime.value.minute);
}
}
_saveNotificationSettings方法将所有响应式变量的当前值写入SharedPreferences。每个设置项都有明确的键名,便于后续维护和调试。时间对象被拆分为hour和minute两个整数存储,这是处理复杂对象持久化的常见模式。所有写入操作都使用await确保完成,避免数据丢失。这个方法在任何设置变更时都会被调用,保证了内存状态与持久化状态的同步。
通知设置页面类定义
创建通知设置页面的主体结构。
class NotificationSettingsPage extends StatelessWidget {
const NotificationSettingsPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('通知设置'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () => _showNotificationInfo(),
),
],
),
NotificationSettingsPage采用StatelessWidget设计,因为所有状态都由NotificationService管理。AppBar包含标题和信息按钮,点击可查看通知设置的详细说明。使用const构造函数优化性能,减少不必要的重建。这种无状态设计配合GetX的响应式状态管理,实现了UI与业务逻辑的清晰分离,提高了代码的可维护性和可测试性。
页面主体布局
构建可滚动的设置内容区域。
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildMasterSwitch(),
SizedBox(height: 16.h),
_buildBasicSettings(),
SizedBox(height: 16.h),
_buildReminderSettings(),
SizedBox(height: 16.h),
_buildCategorySettings(),
SizedBox(height: 16.h),
_buildAdvancedSettings(),
],
),
),
);
}
}
body使用SingleChildScrollView包裹Column,确保内容超出屏幕时可以滚动。padding使用ScreenUtil的.w扩展方法实现屏幕适配,在不同尺寸设备上保持一致的视觉效果。各个设置区域之间使用SizedBox添加16像素的垂直间距,创造清晰的视觉层次。这种模块化的构建方式使得每个设置区域都是独立的组件,便于单独维护和复用。
主开关卡片结构
构建通知主开关的外层容器和图标区域。
Widget _buildMasterSwitch() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Icon(
Icons.notifications_active,
size: 32.sp,
color: const Color(0xFF2196F3),
),
SizedBox(width: 16.w),
主开关使用Card组件创建卡片效果,提供轻微的阴影和圆角,增强视觉层次感。内部使用Row横向排列图标、文字和开关。通知图标使用32像素大小,采用蓝色主题色,使其在视觉上更加突出。这种设计将最重要的控制项放在最显眼的位置,符合用户的操作习惯和视觉焦点。
主开关文字说明
添加主开关的标题和描述文字。
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'应用通知',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4.h),
Text(
'开启后应用将发送通知',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
],
),
),
使用Expanded包裹Column,使文字区域占据剩余空间,确保开关始终在右侧。标题使用18像素粗体,副标题使用14像素灰色文字,形成清晰的信息层次。crossAxisAlignment设置为start,使文字左对齐。这种双层文字设计既提供了功能名称,又给出了简洁的功能说明,帮助用户快速理解每个选项的作用。
主开关控制组件
实现响应式的开关控制逻辑。
Obx(() => Switch(
value: Get.find<NotificationService>().isNotificationEnabled.value,
onChanged: (value) {
Get.find<NotificationService>().isNotificationEnabled.value = value;
Get.find<NotificationService>()._saveNotificationSettings();
},
activeColor: const Color(0xFF2196F3),
)),
],
),
),
);
}
Switch组件使用Obx包裹实现响应式更新,当isNotificationEnabled变化时自动重建。通过Get.find获取NotificationService实例,读取和修改状态。onChanged回调中更新状态值并立即保存到本地存储,确保用户的选择被持久化。activeColor设置为蓝色主题色,与图标颜色保持一致,形成统一的视觉风格。这种即时保存的设计避免了用户忘记保存设置的问题。
基础设置卡片头部
创建基础设置区域的标题和容器。
Widget _buildBasicSettings() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'基础设置',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
基础设置同样使用Card组件包裹,保持与主开关一致的视觉风格。Column的crossAxisAlignment设置为start,使所有子元素左对齐。标题使用18像素粗体,与主开关标题大小一致,建立视觉规律。标题下方添加16像素间距,将标题与具体设置项分隔开。这种分组设计使得相关设置项聚合在一起,提高了界面的可读性和组织性。
通知声音开关
实现通知声音的开关控制。
Obx(() => SwitchListTile(
title: const Text('通知声音'),
subtitle: const Text('播放通知提示音'),
value: Get.find<NotificationService>().isSoundEnabled.value,
onChanged: (value) {
Get.find<NotificationService>().isSoundEnabled.value = value;
Get.find<NotificationService>()._saveNotificationSettings();
},
activeColor: const Color(0xFF2196F3),
)),
const Divider(height: 1),
SwitchListTile是Flutter提供的组合组件,集成了标题、副标题和开关,简化了代码。使用Obx包裹实现响应式更新,确保状态变化时UI同步刷新。onChanged回调中更新状态并保存设置,保持与主开关一致的交互逻辑。Divider组件在选项之间添加分隔线,height设置为1像素,创造清晰的视觉分隔。这种标准化的列表项设计提供了一致的用户体验。
震动和LED设置
添加震动提醒和LED指示灯的开关控制。
Obx(() => SwitchListTile(
title: const Text('震动提醒'),
subtitle: const Text('收到通知时震动'),
value: Get.find<NotificationService>().isVibrationEnabled.value,
onChanged: (value) {
Get.find<NotificationService>().isVibrationEnabled.value = value;
Get.find<NotificationService>()._saveNotificationSettings();
},
activeColor: const Color(0xFF2196F3),
)),
const Divider(height: 1),
Obx(() => SwitchListTile(
title: const Text('LED指示灯'),
subtitle: const Text('使用LED灯闪烁提醒'),
value: Get.find<NotificationService>().isLedEnabled.value,
onChanged: (value) {
Get.find<NotificationService>().isLedEnabled.value = value;
Get.find<NotificationService>()._saveNotificationSettings();
},
activeColor: const Color(0xFF2196F3),
)),
],
),
),
);
}
震动和LED设置采用与声音设置完全相同的结构,保持了代码的一致性和可维护性。每个选项都有清晰的标题和说明,帮助用户理解功能。三个选项之间使用Divider分隔,形成清晰的列表结构。这种重复的模式虽然代码略显冗余,但提供了最直观的用户体验,每个选项都是独立可控的,不会相互影响。LED默认关闭体现了对电量的考虑。
提醒设置卡片结构
创建提醒设置区域的标题和容器。
Widget _buildReminderSettings() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'提醒设置',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
提醒设置区域继续使用Card和Column的组合结构,保持与其他设置区域的一致性。标题样式与基础设置完全相同,建立了统一的视觉语言。这种模块化的设计使得添加新的设置区域变得非常简单,只需复制结构并修改内容即可。一致的间距和排版创造了专业的视觉效果,提升了应用的整体品质感。
每日提醒时间设置
实现每日提醒时间的显示和选择功能。
ListTile(
leading: const Icon(Icons.schedule),
title: const Text('每日提醒时间'),
subtitle: Obx(() => Text(
'每天 ${Get.find<NotificationService>().reminderTime.value.format(Get.context!)} 发送提醒',
)),
trailing: Obx(() => Text(
Get.find<NotificationService>().reminderTime.value.format(Get.context!),
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF2196F3),
),
)),
onTap: () => _showTimePicker(),
),
const Divider(height: 1),
ListTile提供了标准的列表项布局,leading显示时钟图标,title是功能名称,subtitle使用Obx动态显示当前设置的提醒时间。trailing也使用Obx显示时间,采用蓝色突出显示,提示用户这是可点击的。onTap触发时间选择器,让用户修改提醒时间。TimeOfDay的format方法根据系统语言自动格式化时间显示。这种设计既展示了当前设置,又提供了直观的修改入口。
重复提醒设置
添加重复提醒的开关控制。
ListTile(
leading: const Icon(Icons.repeat),
title: const Text('重复提醒'),
subtitle: const Text('未查看的提醒将重复通知'),
trailing: Obx(() => Switch(
value: true, // 这里应该从服务获取
onChanged: (value) {
// 处理重复提醒设置
},
activeColor: const Color(0xFF2196F3),
)),
),
],
),
),
);
}
重复提醒选项使用ListTile配合Switch组件,与基础设置的SwitchListTile效果类似但布局更灵活。leading使用repeat图标,直观表达重复的概念。注释提示这里应该从服务获取真实状态,说明代码还有优化空间。这种设计允许用户选择是否对未查看的通知进行重复提醒,增强了通知的有效性,避免重要事项被遗漏。
时间选择器实现
创建时间选择对话框并处理用户选择。
void _showTimePicker() async {
final service = Get.find<NotificationService>();
final TimeOfDay? pickedTime = await showTimePicker(
context: Get.context!,
initialTime: service.reminderTime.value,
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
child: child!,
);
},
);
_showTimePicker方法使用Flutter内置的showTimePicker显示时间选择对话框。initialTime设置为当前保存的提醒时间,让用户看到当前设置。builder参数用于自定义对话框,通过MediaQuery强制使用24小时制,避免AM/PM的混淆。这种设计考虑了用户体验的一致性,24小时制在大多数场景下更加直观和专业。
时间选择结果处理
处理用户选择的时间并保存设置。
if (pickedTime != null) {
service.reminderTime.value = pickedTime;
await service._saveNotificationSettings();
Get.snackbar('成功', '提醒时间已更新');
}
}
当用户选择了新时间(pickedTime不为null),立即更新服务中的reminderTime状态。调用_saveNotificationSettings保存到本地存储,确保下次启动时能恢复设置。使用Get.snackbar显示成功提示,给用户即时反馈。这种即时保存和反馈的设计模式在移动应用中非常重要,让用户清楚地知道操作已生效,避免了传统表单需要点击保存按钮的繁琐流程。
分类通知卡片结构
创建分类通知设置区域的标题和容器。
Widget _buildCategorySettings() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'分类通知',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
分类通知设置区域继续使用统一的Card布局模式,保持整体界面的一致性。这个区域允许用户针对不同类型的内容设置独立的通知开关,提供了更细粒度的控制。这种分类管理的设计理念来自于用户对不同类型通知的重要性认知不同,例如用户可能希望接收笔记提醒但不希望接收标签提醒。
分类通知选项列表
添加各个分类的通知开关。
_buildCategorySwitch('笔记提醒', 'note_reminder', Icons.note),
const Divider(height: 1),
_buildCategorySwitch('收藏提醒', 'favorite_reminder', Icons.star),
const Divider(height: 1),
_buildCategorySwitch('分类提醒', 'category_reminder', Icons.folder),
const Divider(height: 1),
_buildCategorySwitch('标签提醒', 'tag_reminder', Icons.label),
],
),
),
);
}
这里调用_buildCategorySwitch方法创建四个分类的通知开关,每个分类都有对应的名称、键名和图标。使用Divider在选项之间添加分隔线,创造清晰的列表结构。这种抽取公共方法的做法避免了代码重复,提高了可维护性。四个分类涵盖了记事本应用的主要功能模块,用户可以根据自己的需求灵活配置。
分类开关构建方法
实现创建分类通知开关的通用方法。
Widget _buildCategorySwitch(String title, String key, IconData icon) {
return Obx(() {
final service = Get.find<NotificationService>();
final isEnabled = service.notificationCategories[key] ?? true;
return SwitchListTile(
title: Text(title),
subtitle: Text('接收$title相关的通知'),
leading: Icon(icon),
value: isEnabled,
onChanged: (value) {
service.updateSetting(key, value);
},
activeColor: const Color(0xFF2196F3),
);
});
}
_buildCategorySwitch是一个通用的构建方法,接收标题、键名和图标三个参数。使用Obx包裹整个SwitchListTile,确保状态变化时重建。从notificationCategories Map中读取对应键的值,使用??运算符提供true作为默认值。onChanged回调调用service的updateSetting方法,该方法内部会处理状态更新和持久化。这种参数化的设计使得添加新的分类变得非常简单,只需在调用处添加一行代码即可。
高级设置卡片结构
创建高级设置区域的标题和容器。
Widget _buildAdvancedSettings() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'高级设置',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
高级设置区域采用与其他区域相同的Card布局,保持视觉一致性。这个区域包含了更深层次的功能,如通知优先级、历史记录和系统设置跳转。将这些功能归类为"高级设置",既避免了主界面过于复杂,又为有需求的用户提供了更多控制选项。这种分层设计符合渐进式披露的交互原则。
通知优先级设置
添加通知优先级的导航入口。
ListTile(
leading: const Icon(Icons.priority_high),
title: const Text('通知优先级'),
subtitle: const Text('设置通知的显示优先级'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () => _showPrioritySettings(),
),
const Divider(height: 1),
通知优先级选项使用ListTile布局,leading显示优先级图标,trailing显示向右箭头,暗示这是一个导航操作。点击后调用_showPrioritySettings方法,跳转到优先级设置页面。通知优先级决定了通知在系统中的显示方式和重要程度,高优先级的通知可能会以更显眼的方式展示。这种设计将复杂的设置项放在二级页面,保持主界面的简洁。
通知历史和系统设置
添加通知历史查看和系统设置跳转功能。
ListTile(
leading: const Icon(Icons.history),
title: const Text('通知历史'),
subtitle: const Text('查看最近的通知记录'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () => _showNotificationHistory(),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.settings_applications),
title: const Text('系统设置'),
subtitle: const Text('跳转到系统通知设置'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () => _openSystemSettings(),
),
],
),
),
);
}
通知历史选项允许用户查看过去收到的通知记录,帮助用户回顾重要信息。系统设置选项提供了跳转到系统级通知设置的快捷方式,因为某些权限和设置需要在系统层面配置。这三个选项都使用相同的ListTile布局,保持了界面的一致性。trailing的箭头图标统一使用arrow_forward_ios,这是iOS风格的导航指示器,在Android上也被广泛接受。
权限状态检查
实现通知权限的检查逻辑。
Future<void> _checkNotificationPermissions() async {
final status = await Permission.notification.status;
if (status.isDenied) {
_showPermissionDialog();
} else if (status.isPermanentlyDenied) {
_showPermissionDeniedDialog();
}
}
_checkNotificationPermissions方法使用permission_handler插件检查通知权限状态。根据不同的权限状态采取不同的处理策略:如果权限被拒绝(isDenied),显示请求权限对话框;如果权限被永久拒绝(isPermanentlyDenied),显示引导用户到系统设置的对话框。这种分层处理确保了在各种权限状态下都能给用户提供合适的引导,提高了权限获取的成功率。
权限请求对话框
创建请求通知权限的对话框。
void _showPermissionDialog() {
showDialog(
context: Get.context!,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('需要通知权限'),
content: const Text('为了及时提醒您重要事项,请允许应用发送通知'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
_showPermissionDialog方法使用showDialog显示权限请求对话框。barrierDismissible设置为false,防止用户点击外部区域关闭对话框,确保用户做出明确选择。对话框标题和内容清晰说明了需要权限的原因,这是获取用户信任的关键。提供"取消"按钮尊重用户的选择权,不强制要求授权。这种友好的权限请求方式符合现代应用的最佳实践。
权限请求处理
处理用户的权限授权操作。
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await Permission.notification.request();
},
child: const Text('允许'),
),
],
),
);
}
"允许"按钮使用ElevatedButton突出显示,引导用户点击授权。点击后先关闭对话框,然后调用Permission.notification.request()请求权限。这个方法会触发系统的权限请求对话框,用户在系统对话框中做出最终决定。使用async/await确保操作按序执行。这种两步式的权限请求(应用对话框+系统对话框)虽然增加了步骤,但能更好地向用户解释权限用途,提高授权成功率。
永久拒绝权限处理
处理权限被永久拒绝的情况。
void _showPermissionDeniedDialog() {
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: const Text('通知权限已禁用'),
content: const Text('您已禁用通知权限,请在系统设置中手动开启'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
}
当权限被永久拒绝时,应用无法再次请求权限,只能引导用户到系统设置中手动开启。对话框内容明确告知用户需要在系统设置中操作。"去设置"按钮调用openAppSettings()方法,直接跳转到应用的系统设置页面,减少用户的操作步骤。这种处理方式体现了对用户体验的细致考虑,即使在权限被拒绝的情况下,也提供了清晰的解决路径。
总结与展望
通知设置系统的核心价值在于赋予用户对通知行为的完全控制权。
通过本文的实现,我们构建了一个功能完整的通知设置系统,包括主开关控制、基础设置(声音、震动、LED)、提醒时间设置、分类通知管理、高级功能(优先级、历史、系统设置)以及完善的权限管理。这些功能模块相互配合,形成了一个用户友好的通知管理解决方案。
设计上采用了模块化的卡片布局,每个设置区域都是独立的Card组件,保持了视觉的一致性和代码的可维护性。使用GetX的响应式状态管理,实现了状态变化的自动UI更新,简化了代码逻辑。SharedPreferences确保了设置的持久化,用户的选择在应用重启后依然有效。
权限管理方面,通过分层处理不同的权限状态,提供了友好的权限请求流程。无论是首次请求还是权限被拒绝,都给用户提供了清晰的引导和操作路径。这种细致的权限处理体现了对用户体验的重视。
良好的通知设置不仅提供了丰富的控制选项,更重要的是建立了应用与用户之间的信任关系。通过让用户自主控制通知行为,应用能够在不打扰用户的前提下,及时传递重要信息。这种平衡是现代移动应用成功的关键因素之一。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)