Flutter for OpenHarmony:从零搭建今日资讯App(十六)通知设置功能的实现
本文介绍了如何实现一个层次清晰的通知设置页面。重点包括: 采用三层结构设计:总开关控制全局推送,中间层管理通知类型(新闻/突发/收藏),底层设置提醒方式(声音/振动) 使用StatefulWidget管理开关状态,合理设置默认值(总开关默认开启,收藏通知默认关闭) 实现智能联动逻辑:当总开关关闭时,其他开关自动禁用并变灰,通过三元运算符简洁处理 注重UI细节:用Divider分隔区块,精心设计分组

通知设置是个看起来简单,但细节很多的功能。用户要能控制接收哪些通知,用什么方式提醒。更重要的是,这些开关之间有联动关系,关掉总开关,其他开关也要跟着禁用。今天咱们就来聊聊,怎么把通知设置做得既好用又合理。
通知设置的层次结构
通知设置不是一堆开关堆在一起,而是有层次的。
总开关是推送通知,控制是否接收任何通知。这是最顶层的开关,关掉它,其他所有通知都收不到了。
通知类型包括新闻更新、突发新闻、收藏更新。这些是具体的通知内容,用户可以选择只接收某些类型。但前提是总开关要打开。
提醒方式包括声音和振动。这些控制通知来了怎么提醒用户。同样,总开关关了,这些设置也没意义。
这种层次结构很重要,让用户能快速理解每个开关的作用。
为什么用StatefulWidget
通知设置页面需要管理多个开关的状态,所以用StatefulWidget:
class NotificationSettingsScreen extends StatefulWidget {
const NotificationSettingsScreen({super.key});
State<NotificationSettingsScreen> createState() =>
_NotificationSettingsScreenState();
}
有人可能会问,为什么不用Provider?因为通知设置是页面级的状态,不需要跨页面共享。用StatefulWidget更简单,不用引入额外的依赖。
如果以后要把设置保存到本地,或者在其他页面显示通知状态,那时候再改成Provider也不迟。
状态变量的定义
先定义所有开关的状态:
class _NotificationSettingsScreenState extends State<NotificationSettingsScreen> {
bool _pushEnabled = true;
bool _newsUpdates = true;
bool _breakingNews = true;
bool _favorites = false;
bool _soundEnabled = true;
bool _vibrationEnabled = true;
每个开关对应一个bool变量。变量名要见名知意,一看就知道是什么开关。
_pushEnabled是总开关,默认打开。大部分用户都希望接收通知,所以默认值是true。
_newsUpdates和**_breakingNews默认打开,因为这是新闻应用的核心功能。_favorites**默认关闭,因为不是所有用户都会用收藏功能。
_soundEnabled和**_vibrationEnabled**默认打开,这是最常见的提醒方式。
默认值的选择很重要,要符合大部分用户的习惯。
页面整体结构
通知设置页面用ListView包含多个分组:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('通知设置'),
),
body: ListView(
children: [
SwitchListTile(...), // 总开关
const Divider(),
Padding(...), // 分组标题
SwitchListTile(...), // 通知类型
SwitchListTile(...),
SwitchListTile(...),
const Divider(),
Padding(...), // 分组标题
SwitchListTile(...), // 提醒方式
SwitchListTile(...),
],
),
);
}
用ListView而不是Column,因为内容可能超出屏幕高度。用Divider分隔不同分组,用Padding包裹分组标题。
这个结构清晰,用户一眼就能看出哪些开关是一组的。
总开关的实现
总开关是最重要的,单独放在最上面:
SwitchListTile(
title: const Text('推送通知'),
subtitle: const Text('接收应用推送通知'),
value: _pushEnabled,
onChanged: (value) {
setState(() {
_pushEnabled = value;
});
},
)
SwitchListTile是带开关的ListTile,Flutter提供的标准组件。title是主标题,subtitle是副标题,解释这个开关的作用。
value绑定到**_pushEnabled**,开关的状态和变量同步。onChanged是开关切换时的回调,在里面调用setState更新状态。
注意这里没有任何联动逻辑,只是简单地更新状态。联动逻辑在其他开关的onChanged里处理。
分组标题的实现
分组标题用Padding包裹Text:
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'通知类型',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
)
padding: 16让标题不贴边,视觉上更舒服。fontSize: 14比正文小一点,表示这是标题。fontWeight: FontWeight.bold加粗更醒目。
color: primary使用主题色,和应用风格一致。这样深色模式下颜色也会自动适配。
通知类型开关的联动
通知类型的开关要和总开关联动:
SwitchListTile(
title: const Text('新闻更新'),
subtitle: const Text('接收最新新闻推送'),
value: _newsUpdates,
onChanged: _pushEnabled
? (value) {
setState(() {
_newsUpdates = value;
});
}
: null,
)
关键在onChanged的处理。如果**_pushEnabled是true,onChanged是一个正常的回调函数。如果_pushEnabled**是false,onChanged是null。
onChanged为null时,开关会自动禁用,显示为灰色,用户点击没反应。这是Flutter的标准行为,不用自己写禁用逻辑。
这种联动很直观:总开关关了,其他开关自动变灰,用户一眼就能看出它们不可用。
三元运算符的妙用
注意这里用了三元运算符:
onChanged: _pushEnabled ? (value) { ... } : null
这比if-else简洁多了。如果用if-else:
onChanged: () {
if (_pushEnabled) {
return (value) {
setState(() {
_newsUpdates = value;
});
};
} else {
return null;
}
}()
代码又长又难读。三元运算符一行搞定,清晰明了。
突发新闻开关
突发新闻的实现和新闻更新一样:
SwitchListTile(
title: const Text('突发新闻'),
subtitle: const Text('接收重要突发新闻'),
value: _breakingNews,
onChanged: _pushEnabled
? (value) {
setState(() {
_breakingNews = value;
});
}
: null,
)
这里有个细节:subtitle写的是"接收重要突发新闻",而不是"接收突发新闻"。加个"重要",暗示这种通知不会太频繁,用户更愿意打开。
文案很重要,好的文案能提高用户的接受度。
收藏更新开关
收藏更新默认关闭,因为不是所有用户都会用:
SwitchListTile(
title: const Text('收藏更新'),
subtitle: const Text('收藏的新闻有更新时通知'),
value: _favorites,
onChanged: _pushEnabled
? (value) {
setState(() {
_favorites = value;
});
}
: null,
)
subtitle解释得很清楚:“收藏的新闻有更新时通知”。用户一看就懂,不会困惑这个开关是干什么的。
提醒方式的开关
提醒方式包括声音和振动:
SwitchListTile(
title: const Text('声音'),
value: _soundEnabled,
onChanged: _pushEnabled
? (value) {
setState(() {
_soundEnabled = value;
});
}
: null,
)
注意这里没有subtitle,因为"声音"两个字已经很清楚了,不需要额外解释。
有subtitle的开关占两行,没有subtitle的占一行。合理使用subtitle,让页面既清晰又紧凑。
振动开关也一样:
SwitchListTile(
title: const Text('振动'),
value: _vibrationEnabled,
onChanged: _pushEnabled
? (value) {
setState(() {
_vibrationEnabled = value;
});
}
: null,
)
开关联动的逻辑分析
咱们的联动逻辑很简单:总开关关了,其他开关禁用。但实际项目中,联动可能更复杂。
场景一:关闭总开关时,同时关闭所有子开关
onChanged: (value) {
setState(() {
_pushEnabled = value;
if (!value) {
_newsUpdates = false;
_breakingNews = false;
_favorites = false;
_soundEnabled = false;
_vibrationEnabled = false;
}
});
}
这样用户重新打开总开关时,需要重新选择要接收哪些通知。有些应用这么做,但用户体验不太好。
场景二:记住子开关的状态
Map<String, bool> _savedStates = {};
onChanged: (value) {
setState(() {
if (!value) {
// 保存当前状态
_savedStates = {
'newsUpdates': _newsUpdates,
'breakingNews': _breakingNews,
'favorites': _favorites,
'soundEnabled': _soundEnabled,
'vibrationEnabled': _vibrationEnabled,
};
} else {
// 恢复之前的状态
_newsUpdates = _savedStates['newsUpdates'] ?? true;
_breakingNews = _savedStates['breakingNews'] ?? true;
_favorites = _savedStates['favorites'] ?? false;
_soundEnabled = _savedStates['soundEnabled'] ?? true;
_vibrationEnabled = _savedStates['vibrationEnabled'] ?? true;
}
_pushEnabled = value;
});
}
这样用户关闭总开关再打开,子开关还是原来的状态。体验更好,但代码更复杂。
咱们的应用用最简单的方式:只禁用,不修改状态。用户重新打开总开关,子开关还是原来的状态。
数据持久化
通知设置应该保存到本地,下次打开应用还能记住。用SharedPreferences:
import 'package:shared_preferences/shared_preferences.dart';
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_pushEnabled = prefs.getBool('pushEnabled') ?? true;
_newsUpdates = prefs.getBool('newsUpdates') ?? true;
_breakingNews = prefs.getBool('breakingNews') ?? true;
_favorites = prefs.getBool('favorites') ?? false;
_soundEnabled = prefs.getBool('soundEnabled') ?? true;
_vibrationEnabled = prefs.getBool('vibrationEnabled') ?? true;
});
}
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('pushEnabled', _pushEnabled);
await prefs.setBool('newsUpdates', _newsUpdates);
await prefs.setBool('breakingNews', _breakingNews);
await prefs.setBool('favorites', _favorites);
await prefs.setBool('soundEnabled', _soundEnabled);
await prefs.setBool('vibrationEnabled', _vibrationEnabled);
}
在initState里加载,在onChanged里保存:
void initState() {
super.initState();
_loadSettings();
}
onChanged: _pushEnabled
? (value) {
setState(() {
_newsUpdates = value;
});
_saveSettings();
}
: null
这样设置就能持久化了。
用Provider管理通知设置
如果通知设置要在多个页面使用,可以改用Provider:
class NotificationProvider extends ChangeNotifier {
bool _pushEnabled = true;
bool _newsUpdates = true;
bool _breakingNews = true;
bool _favorites = false;
bool _soundEnabled = true;
bool _vibrationEnabled = true;
bool get pushEnabled => _pushEnabled;
bool get newsUpdates => _newsUpdates;
bool get breakingNews => _breakingNews;
bool get favorites => _favorites;
bool get soundEnabled => _soundEnabled;
bool get vibrationEnabled => _vibrationEnabled;
NotificationProvider() {
_loadSettings();
}
Future<void> setPushEnabled(bool value) async {
_pushEnabled = value;
await _saveSettings();
notifyListeners();
}
Future<void> setNewsUpdates(bool value) async {
_newsUpdates = value;
await _saveSettings();
notifyListeners();
}
// 其他setter方法...
}
页面里用Consumer:
Consumer<NotificationProvider>(
builder: (context, provider, child) {
return SwitchListTile(
title: const Text('推送通知'),
value: provider.pushEnabled,
onChanged: (value) {
provider.setPushEnabled(value);
},
);
},
)
Provider的好处是状态全局共享,其他页面也能访问。比如在首页显示通知状态,或者在详情页根据设置决定是否发送通知。
通知权限的处理
Android和iOS都需要用户授权才能发送通知。可以用permission_handler插件:
import 'package:permission_handler/permission_handler.dart';
Future<void> _requestNotificationPermission() async {
final status = await Permission.notification.status;
if (status.isDenied) {
final result = await Permission.notification.request();
if (result.isGranted) {
// 权限已授予
} else if (result.isPermanentlyDenied) {
// 用户永久拒绝,引导去设置页面
_showPermissionDialog();
}
}
}
void _showPermissionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('需要通知权限'),
content: const Text('请在系统设置中开启通知权限'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
openAppSettings();
Navigator.pop(context);
},
child: const Text('去设置'),
),
],
),
);
}
用户打开总开关时,先检查权限:
onChanged: (value) async {
if (value) {
await _requestNotificationPermission();
}
setState(() {
_pushEnabled = value;
});
}
通知的实际发送
设置只是第一步,还要能真的发送通知。可以用flutter_local_notifications插件:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> _initNotifications() async {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings();
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await flutterLocalNotificationsPlugin.initialize(settings);
}
Future<void> _showNotification(String title, String body) async {
const androidDetails = AndroidNotificationDetails(
'news_channel',
'新闻通知',
channelDescription: '接收最新新闻推送',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await flutterLocalNotificationsPlugin.show(
0,
title,
body,
details,
);
}
根据用户的设置决定是否发送:
Future<void> sendNewsNotification(String title, String body) async {
if (!_pushEnabled || !_newsUpdates) return;
await _showNotification(title, body);
}
通知的分组管理
Android支持通知分组,可以把相同类型的通知归到一起:
const androidDetails = AndroidNotificationDetails(
'news_channel',
'新闻通知',
groupKey: 'news_group',
setAsGroupSummary: false,
);
还可以创建一个汇总通知:
const summaryDetails = AndroidNotificationDetails(
'news_channel',
'新闻通知',
groupKey: 'news_group',
setAsGroupSummary: true,
);
这样多条新闻通知会自动折叠,不会刷屏。
通知的声音和振动
根据用户设置控制声音和振动:
final androidDetails = AndroidNotificationDetails(
'news_channel',
'新闻通知',
playSound: _soundEnabled,
enableVibration: _vibrationEnabled,
sound: _soundEnabled
? const RawResourceAndroidNotificationSound('notification')
: null,
);
自定义通知声音需要把音频文件放在**android/app/src/main/res/raw/**目录下。
定时通知
可以设置定时发送通知:
await flutterLocalNotificationsPlugin.zonedSchedule(
0,
'早报',
'今日新闻精选',
_nextInstanceOfTime(8, 0), // 每天早上8点
const NotificationDetails(
android: AndroidNotificationDetails(
'daily_channel',
'每日推送',
),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
);
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
var scheduledDate = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
hour,
minute,
);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
一些实用的扩展
通知设置还可以加很多实用功能。
免打扰时段:
TimeOfDay _doNotDisturbStart = const TimeOfDay(hour: 22, minute: 0);
TimeOfDay _doNotDisturbEnd = const TimeOfDay(hour: 7, minute: 0);
bool _isInDoNotDisturbPeriod() {
final now = TimeOfDay.now();
// 判断当前时间是否在免打扰时段
return _isTimeBetween(now, _doNotDisturbStart, _doNotDisturbEnd);
}
通知频率限制:
DateTime? _lastNotificationTime;
final _minInterval = const Duration(minutes: 30);
bool _canSendNotification() {
if (_lastNotificationTime == null) return true;
return DateTime.now().difference(_lastNotificationTime!) > _minInterval;
}
通知预览:
ElevatedButton(
onPressed: () {
_showNotification('测试通知', '这是一条测试通知');
},
child: const Text('发送测试通知'),
)
常见问题
开关状态不同步:可能是忘记调用setState。确保每次修改状态都调用setState。
通知发不出来:检查权限是否授予,通知渠道是否创建,设置是否正确。
联动逻辑不生效:检查onChanged是否正确处理了null的情况。
写在最后
通知设置看起来简单,但要做好不容易。开关联动、数据持久化、权限处理、实际发送,每个环节都要考虑周全。
最重要的是用户体验。开关要清晰,联动要合理,文案要友好。这些细节做好了,用户才会愿意打开通知。
代码写完了,记得多测试。不同的开关组合会不会有问题?关闭总开关再打开,状态对不对?通知能不能正常发送?这些场景都要测试到。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。
更多推荐
所有评论(0)