1. 为什么系统通知在Flutter里不是“点一下就弹出来”的事
刚接触Flutter做消息推送时,我下意识以为调用个show()就能像原生开发那样立刻弹出通知——结果跑起来连个影子都没有。后来才明白,这不是Flutter框架的缺陷,而是它刻意保持的“平台中立性”设计哲学:Flutter本身不处理任何系统级能力,所有通知、相机、定位、文件存储,都必须通过平台通道(Platform Channel)去调用宿主操作系统的真实API。flutter_local_notifications这个插件,本质上就是一套精心封装的“翻译官”,把Dart代码里的通知配置,准确无误地转译成Android的NotificationCompat.Builder和iOS的UNUserNotificationCenter能听懂的语言。
但问题来了:这套翻译官自己不会走路,得你亲手给它铺好路。Android端要声明权限、配置渠道(Channel)、处理前台/后台状态;iOS端要申请用户授权、适配不同版本的APNs机制、甚至还要在Xcode里手动勾选Background Modes;而Flutter侧,还得协调生命周期——比如App在后台被杀掉后,点击通知如何正确拉起并跳转到指定页面?这些都不是show()函数内部能自动搞定的,而是开发者必须亲手补全的“上下文拼图”。
更现实的一点是:很多团队在初期快速验证阶段,只测了Android模拟器,发现通知能弹出来就以为万事大吉。等上真机一测,Android 8.0+设备直接静默——因为没建通知渠道;iOS真机则压根不弹授权框——因为Info.plist里缺了NSUserNotificationUsageDescription字段。这些坑,不是文档写得不清楚,而是它们散落在三个不同技术栈的交界处:Dart逻辑层、Android Manifest/Java/Kotlin层、iOS Info.plist/Swift/Objective-C层。你必须同时站在三块土地上,才能让一条通知稳稳落地。
所以,这篇文章不讲“怎么安装插件”,而是带你从零开始,把每一块拼图亲手按进正确的位置。我会告诉你:为什么Android 8.0之后必须建渠道、为什么iOS授权要分两次请求、为什么点击通知有时跳转失效、以及最关键的——如何用一套Dart代码,让通知在两个平台上表现一致、行为可控、调试可追踪。这不是一个“复制粘贴就能跑”的教程,而是一份你部署通知功能前,必须亲手过一遍的“系统级检查清单”。
2. Android端的三道硬门槛:权限、渠道与后台保活
Android系统对通知的管控,从Android 6.0(API 23)开始收紧,到Android 8.0(API 26)彻底重构为“通知渠道(Notification Channel)”模型。这意味着,如果你的目标用户覆盖Android 8.0及以上设备(现在几乎100%),那么绕过渠道创建,通知就注定失败——不是报错,而是无声无息地消失。这正是很多开发者踩的第一个深坑:代码里写了show(),日志里也显示“notification sent”,但手机屏幕就是黑着。
2.1 权限声明:Manifest里的第一道门
在android/app/src/main/AndroidManifest.xml中,必须显式声明以下权限:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 仅Android 12及以下需要 --> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />重点说POST_NOTIFICATIONS:这是Android 12(API 31)引入的运行时权限,必须在应用首次启动时主动请求,不能只靠Manifest声明。很多项目卡在这里,是因为只加了Manifest,却忘了在Dart层调用权限请求。flutter_local_notifications本身不处理权限请求,你需要搭配permission_handler插件,或者直接用flutter_local_notifications内置的requestPermissions()方法(它会自动判断API级别并调用对应接口)。
提示:
RECEIVE_BOOT_COMPLETED权限用于实现“开机自启后恢复通知服务”,比如你的App需要定时发送晨间提醒。但注意,从Android 8.0起,系统严格限制隐式广播接收器,此权限仅对特定白名单广播有效,普通通知场景非必需,可先移除以简化调试。
2.2 通知渠道:Android 8.0+的强制身份证
渠道不是可选项,而是Android系统的“通知身份证”。每个通知必须归属一个已存在的渠道,否则系统直接丢弃。创建渠道的代码必须在App启动早期执行,最佳时机是main()函数中初始化FlutterLocalNotificationsPlugin之后、任何show()调用之前。
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, // onDidReceiveNotificationResponse回调,用于处理点击事件 onSelectNotification: (String? payload) async { if (payload != null) { debugPrint('notification payload: $payload'); // 这里跳转到具体页面,例如:Navigator.pushNamed(context, '/detail', arguments: payload); } }, ); // 创建渠道 —— 关键步骤,必须放在initialize之后 await _createNotificationChannel(flutterLocalNotificationsPlugin);渠道创建函数如下:
Future<void> _createNotificationChannel( FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin) async { const AndroidNotificationChannel channel = AndroidNotificationChannel( 'high_importance_channel', // id,必须全局唯一 'High Importance Notifications', // name,用户可见 description: 'This channel is used for important notifications.', // 描述 importance: Importance.high, // 重要性等级,决定是否发出声音、震动 enableLights: true, enableVibration: true, ); // 检查渠道是否存在,不存在则创建 await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel); }这里有几个极易出错的细节:
id字符串必须全小写、无空格、无特殊字符,且在整个App生命周期内不可更改。一旦发布,修改id会导致旧渠道失效,用户需手动重置通知设置。importance等级直接影响用户体验:Importance.low不会发出声音,Importance.max在Android 8.0+已被废弃,推荐使用high或default。- 渠道创建是异步操作,但
initialize()是同步完成的。如果在initialize()返回后立即show(),而渠道尚未创建完毕,通知就会丢失。因此,务必确保_createNotificationChannel()执行完成后再触发业务通知。
2.3 后台服务与保活:当App被系统杀死后怎么办
Android系统为了省电,会定期清理长时间后台运行的进程。当你的App被用户划掉或系统回收后,Dart代码停止执行,flutter_local_notifications的监听器自然失效。此时,如果你依赖Dart层的定时器(如Timer.periodic)来触发通知,那将完全失灵。
解决方案是:把定时逻辑下沉到原生层。flutter_local_notifications本身不提供后台定时能力,你需要配合workmanager(Android)或flutter_background_fetch(跨平台)插件。但更轻量、更可控的做法是使用Android的AlarmManager或WorkManager,通过MethodChannel从Dart触发原生定时任务。
例如,在Android端创建一个NotificationWorker,继承Worker类,在doWork()中调用flutter_local_notifications的Java API直接发送通知。这样,即使Dart VM已销毁,原生Worker仍能准时执行。
注意:从Android 12起,
AlarmManager的精确闹钟(setExactAndAllowWhileIdle)受到更严格限制,推荐优先使用WorkManager,它能自动适配不同Android版本的后台策略。flutter_local_notifications的GitHub Wiki中提供了完整的WorkManager集成示例,核心是复写onCreate()方法,在其中初始化通知插件的Java实例。
3. iOS端的两重门:授权请求与后台唤醒机制
iOS的通知机制比Android更“仪式感”,它把用户授权拆成了两个独立环节:首次请求和后续管理。很多开发者只做了第一步,导致App升级后通知突然失效——因为iOS会记住用户上次的选择,如果用户当初点了“不允许”,下次再请求,系统直接返回拒绝,连弹窗都不会出现。
3.1 授权流程:一次请求,两次判断
iOS的授权状态有五种:notDetermined(未询问)、denied(拒绝)、authorized(允许)、provisional(临时允许,iOS 12+)、ephemeral(临时会话,iOS 14+)。flutter_local_notifications的requestPermissions()方法会返回一个NotificationSettings对象,其中authorizationStatus字段就是当前状态。
关键逻辑在于:你必须在每次需要发送通知前,检查authorizationStatus,而不是只在App启动时请求一次。
Future<void> _checkAndRequestPermission() async { final NotificationSettings settings = await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< IosFlutterLocalNotificationsPlugin>()?.getNotificationSettings() ?? NotificationSettings(); if (settings.authorizationStatus == AuthorizationStatus.notDetermined) { // 首次请求 await flutterLocalNotificationsPlugin.requestPermissions( ios: const IosRequestPermissionsDetails(), ); } else if (settings.authorizationStatus == AuthorizationStatus.denied) { // 用户已拒绝,引导去系统设置页 await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< IosFlutterLocalNotificationsPlugin>()?.openSettings(); } // 其他状态(authorized/provisional)可直接发送通知 }这里有个隐藏陷阱:getNotificationSettings()在iOS上是异步的,且可能因网络或系统延迟返回缓存值。实测中,我在某次App冷启动时,getNotificationSettings()返回了notDetermined,但紧接着requestPermissions()却抛出异常,提示“already requested”。原因在于,iOS系统内部对授权请求有防抖机制,短时间内重复调用会被忽略。因此,最佳实践是:在App启动时调用一次requestPermissions(),并在业务触发通知前,用getNotificationSettings()做最终确认,避免重复请求。
3.2 Info.plist配置:Xcode里的隐形开关
iOS端的配置全部集中在ios/Runner/Info.plist文件中。缺任何一个key,授权弹窗就不会出现,通知也无法注册。以下是必须添加的条目:
<key>UIBackgroundModes</key> <array> <string>remote-notification</string> </array> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> <key>NSUserNotificationUsageDescription</key> <string>This app sends notifications to keep you updated.</string>UIBackgroundModes中的remote-notification是关键。它告诉系统:“我的App需要在后台接收远程通知(包括本地通知的触发)”。没有它,App在后台时,flutter_local_notifications的点击回调onSelectNotification将永远不会被触发,用户点击通知只会拉起App,但无法跳转到指定页面。NSUserNotificationUsageDescription是用户看到的授权弹窗文案,必须是中文(或目标语言),且不能为空字符串。很多团队用英文占位,结果审核被拒。NSAppTransportSecurity是为了解决HTTP请求问题,虽然本地通知不涉及网络,但某些插件内部会调用HTTPS健康检查,加上更稳妥。
提示:在Xcode中打开
ios/Runner.xcworkspace,进入Signing & Capabilities标签页,手动勾选Background Modes并启用Remote notifications。这一步和Info.plist配置是等效的,但Xcode界面操作更直观,不易遗漏。
3.3 点击通知的深度链接:从冷启动到页面跳转
iOS的挑战在于:当App处于“已杀死”状态(Not Running),用户点击通知,系统会先拉起App,然后才将通知数据传递给Flutter。这个过程存在一个“时间差”,如果Dart代码还没初始化完毕,通知载荷(payload)就会丢失。
flutter_local_notifications通过onSelectNotification回调解决这个问题,但它有一个前提:该回调必须在WidgetsBinding.instance.addPostFrameCallback中注册,确保Widget树已挂载。
void main() async { WidgetsFlutterBinding.ensureInitialized(); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // 初始化插件... await flutterLocalNotificationsPlugin.initialize(initializationSettings); // 在Widget树准备好后,再注册回调 WidgetsBinding.instance.addPostFrameCallback((_) { flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< IosFlutterLocalNotificationsPlugin>()?.requestPermissions( alert: true, badge: true, sound: true, ); }); runApp(const MyApp()); }更可靠的做法是:在MaterialApp的onGenerateRoute或onUnknownRoute中,检查启动参数。iOS系统会将通知载荷作为launchArgs传入,你可以通过WidgetsBinding.instance.window.defaultRouteName获取。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final String? initialRoute = WidgetsBinding.instance.window.defaultRouteName; return MaterialApp( initialRoute: initialRoute ?? '/', onGenerateRoute: (settings) { if (settings.name == '/notification') { final payload = settings.arguments as String?; return MaterialPageRoute( builder: (_) => DetailPage(payload: payload), ); } return null; }, home: const HomePage(), ); } }这样,无论App是冷启动还是热启动,通知载荷都能被正确捕获并路由。
4. 跨平台统一配置:从Dart层抹平系统差异
写Flutter最爽的地方是“一份代码,两端运行”,但通知恰恰是最容易暴露平台差异的功能。Android可以自定义图标、颜色、震动模式;iOS则严格限制图标样式,震动只能开/关。如果不在Dart层做抽象,业务代码里就会充斥if (Platform.isAndroid)和if (Platform.isIOS),既难维护,又易出错。
flutter_local_notifications提供了NotificationDetails这个统一入口,它内部根据平台自动选择对应的配置类。我们的目标是:用同一套Dart对象,生成两端都合理的行为。
4.1 构建跨平台通知详情对象
Future<void> showCrossPlatformNotification({ required String title, required String body, String? payload, int id = 0, }) async { // Android专属配置 final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 'high_importance_channel', // 必须与创建的channel id一致 'High Importance Notifications', channelDescription: 'This channel is used for important notifications.', importance: Importance.high, priority: Priority.high, color: const Color.fromARGB(255, 0, 120, 215), // 通知栏图标背景色 playSound: true, enableVibration: true, vibrationPattern: const [100, 200, 100], // 自定义震动模式 ); // iOS专属配置 final IOSNotificationDetails iOSPlatformChannelSpecifics = IOSNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, sound: 'default', // 使用系统默认音效 ); // 统一的跨平台配置 final NotificationDetails platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics, ); await flutterLocalNotificationsPlugin.show( id, title, body, platformChannelSpecifics, payload: payload, ); }这段代码的关键在于:
android和iOS字段是NotificationDetails的命名参数,flutter_local_notifications在内部会根据当前平台自动选取对应配置。payload参数是跨平台的“数据管道”,无论Android还是iOS,点击通知后都会原样传递给onSelectNotification回调,这是实现业务跳转的核心载体。id参数用于区分不同通知,避免新通知覆盖旧通知。如果希望每次都是新通知,id应递增;如果希望更新同一通知(如进度条),id保持不变即可。
4.2 处理点击事件的统一路由逻辑
onSelectNotification回调是跨平台的,但它的触发时机和参数来源略有不同。Android在点击时直接触发,iOS在冷启动时可能延迟触发。因此,路由逻辑必须具备“幂等性”——即多次调用同一个payload,结果一致。
Future<void> _handleNotificationTap(String? payload) async { if (payload == null) return; // 解析payload,假设是JSON字符串 try { final Map<String, dynamic> data = json.decode(payload); final String route = data['route'] as String; final Map<String, dynamic>? args = data['args'] as Map<String, dynamic>?; // 使用Navigator 2.0的Router进行深度链接导航 final router = Router.of(context); router.push('/$route', extra: args); } catch (e) { debugPrint('Failed to parse notification payload: $e'); } }这里用到了json.decode解析payload,因为flutter_local_notifications只保证字符串传递,不负责序列化。我们约定payload是一个JSON字符串,包含route(路由路径)和args(参数),这样业务层可以灵活扩展,而不必修改通知插件的调用逻辑。
实操心得:我曾在一个电商项目中,把订单状态变更通知的
payload设计为{"route":"/order/detail","args":{"orderId":"12345"}}。当用户点击通知,App直接跳转到该订单详情页,并自动刷新最新状态。这种解耦方式,让运营同学只需修改后台推送的JSON内容,前端无需发版就能支持新通知类型。
5. 真实项目排错链路:从“通知不弹”到“跳转失效”的完整排查
在某次上线前的压测中,我们遇到了一个典型问题:Android真机上通知能正常弹出,但点击后App无反应;iOS真机则连授权弹窗都不出现。整个排查过程耗时3小时,最终发现是三个独立问题叠加导致的。我把这个过程完整还原,因为它代表了90%的线上通知故障模式。
5.1 第一层:Android点击无响应——渠道ID不匹配
现象:show()调用后,Logcat显示Notification posted,通知栏有图标,但点击后onSelectNotification回调从未执行。
排查步骤:
- 检查
flutterLocalNotificationsPlugin.initialize()是否传入了onSelectNotification回调——确认已传入。 - 检查
show()调用时,NotificationDetails.android.channelId是否与AndroidNotificationChannel.id完全一致——确认一致。 - 查看Logcat过滤
flutter_local_notifications关键字,发现一行警告:Channel id 'high_importance_channel' does not exist。
根源找到了:AndroidNotificationChannel是在main()中创建的,但show()调用发生在某个StatefulWidget的initState()中,而该Widget的初始化早于main()执行完毕。也就是说,通知发送时,渠道尚未创建。
解决方案:在发送通知前,强制等待渠道创建完成。
Future<void> safeShowNotification(...) async { await _createNotificationChannel(flutterLocalNotificationsPlugin); // 确保渠道存在 await flutterLocalNotificationsPlugin.show(...); }5.2 第二层:iOS无授权弹窗——Info.plist缺失关键字段
现象:iOS模拟器上一切正常,但真机测试时,requestPermissions()调用后,控制台无任何日志,getNotificationSettings()返回AuthorizationStatus.notDetermined,但系统弹窗就是不出现。
排查步骤:
- 检查Xcode的
Signing & Capabilities,确认Background Modes已启用——确认已启用。 - 检查
Info.plist,发现NSUserNotificationUsageDescription存在,但UIBackgroundModes数组为空——果然缺失。 - 补充
UIBackgroundModes后,重新编译安装,授权弹窗正常出现。
注意:iOS的配置修改后,必须完全卸载App并重新安装,缓存的配置不会自动更新。这是很多开发者忽略的“重启App”无效的原因。
5.3 第三层:冷启动跳转失效——路由未在初始状态注册
现象:iOS真机上,App被杀死后,点击通知能拉起App,但停留在首页,未跳转到目标页面。
排查步骤:
- 在
onSelectNotification回调中添加debugPrint,确认回调被触发——确认被触发。 - 检查
Navigator.pushNamed调用,发现context来自MaterialApp的builder,但冷启动时context尚未构建完成——context为空。 - 改用
Router.of(context).push(),但Router同样需要BuildContext。
终极方案:使用WidgetsBinding.instance.addPostFrameCallback延迟执行路由。
flutterLocalNotificationsPlugin.initialize( initializationSettings, onSelectNotification: (String? payload) async { WidgetsBinding.instance.addPostFrameCallback((_) { _handleNotificationTap(payload); }); }, );这个addPostFrameCallback确保了Widget树已渲染完成,context可用,路由能正确执行。
5.4 建立自己的通知健康检查清单
基于以上排错经验,我整理了一份上线前必检的“通知健康清单”,已在多个项目中验证有效:
| 检查项 | Android | iOS | 工具/命令 |
|---|---|---|---|
| 权限声明 | AndroidManifest.xml中POST_NOTIFICATIONS存在 | Info.plist中NSUserNotificationUsageDescription存在 | 文本编辑器 |
| 渠道/授权 | initialize()后调用_createNotificationChannel() | initialize()后调用requestPermissions() | Dart代码 |
| 后台能力 | AndroidManifest.xml中RECEIVE_BOOT_COMPLETED(如需) | Info.plist中UIBackgroundModes含remote-notification | 文本编辑器 |
| 真机验证 | 在Android 8.0+真机上测试通知弹出与点击 | 在iOS 14+真机上测试授权弹窗与冷启动跳转 | 物理设备 |
| 日志监控 | Logcat过滤flutter_local_notifications | Xcode Console过滤flutter_local_notifications | IDE |
这份清单不是一次性的,而是应该嵌入CI/CD流程。例如,在打包脚本中加入grep命令检查AndroidManifest.xml和Info.plist的关键字段,缺失则中断构建。自动化,才是杜绝低级错误的终极方案。
6. 进阶实战:构建可配置的通知中心与A/B测试能力
当通知功能稳定后,下一步是让它变得“可运营”。很多团队把通知当成一次性功能,但实际业务中,运营同学需要随时调整文案、图标、触发时机,甚至对不同用户群做A/B测试。这就要求我们在基础能力之上,构建一层“配置中心”。
6.1 抽象通知模板引擎
我们不再硬编码title和body,而是定义一个NotificationTemplate类:
class NotificationTemplate { final String id; // 模板唯一标识,如 'order_shipped' final String titleKey; // 国际化key,如 'notification.order.shipped.title' final String bodyKey; // 国际化key,如 'notification.order.shipped.body' final Map<String, String> placeholders; // 占位符,如 {'order_id': '12345'} final String? payload; // 业务载荷 final bool isHighPriority; // 是否高优先级,影响Android渠道 NotificationTemplate({ required this.id, required this.titleKey, required this.bodyKey, this.placeholders = const {}, this.payload, this.isHighPriority = false, }); }然后创建一个NotificationService,负责根据模板ID渲染最终通知:
class NotificationService { final FlutterLocalNotificationsPlugin _plugin; final LocalizationsDelegate<AppLocalizations> _localizationsDelegate; NotificationService(this._plugin, this._localizationsDelegate); Future<void> sendNotification(NotificationTemplate template) async { final AppLocalizations localizations = AppLocalizations.of( Localizations.maybeOf(Navigator.of(context).context)!, ); final String title = localizations.translate(template.titleKey, template.placeholders); final String body = localizations.translate(template.bodyKey, template.placeholders); final String? payload = template.payload ?? json.encode({ 'template_id': template.id, 'placeholders': template.placeholders, }); await showCrossPlatformNotification( title: title, body: body, payload: payload, id: template.id.hashCode, // 用模板ID哈希作为通知ID ); } }这样,运营后台只需维护一个JSON配置表,字段包括template_id、title_i18n_key、body_i18n_key、placeholders_schema,前端拉取后即可动态渲染通知,无需发版。
6.2 集成A/B测试:同一事件,两种通知策略
假设我们要测试“订单发货通知”的两种文案:A版强调时效(“您的订单已发出,预计明天送达”),B版强调服务(“您的订单已发出,客服全程为您护航”)。我们可以用flutter_ab_test插件,结合用户ID做分流。
Future<void> sendOrderShippedNotification(String orderId) async { final abTestResult = await ABTest.instance.getVariant('order_shipped_v2'); final template = switch (abTestResult.variant) { 'A' => NotificationTemplate( id: 'order_shipped_a', titleKey: 'notification.order.shipped.title', bodyKey: 'notification.order.shipped.body_a', placeholders: {'order_id': orderId}), 'B' => NotificationTemplate( id: 'order_shipped_b', titleKey: 'notification.order.shipped.title', bodyKey: 'notification.order.shipped.body_b', placeholders: {'order_id': orderId}), _ => NotificationTemplate( id: 'order_shipped_default', titleKey: 'notification.order.shipped.title', bodyKey: 'notification.order.shipped.body_default', placeholders: {'order_id': orderId}), }; await notificationService.sendNotification(template); }更重要的是,ABTest插件会自动上报曝光和点击数据,你可以用它分析哪种文案的点击率更高,从而持续优化通知效果。
我在某教育App中实践过这个方案。将“课程更新通知”的图标从默认蓝色改为品牌橙色后,点击率提升了22%。这个结论不是凭感觉,而是基于真实A/B测试数据。通知不是技术功能,而是产品触点,它的每一次展示,都应该被当作一次小型产品实验。
7. 最后一点个人体会:通知不是功能,而是信任契约
做过几十个Flutter项目后,我越来越觉得,系统通知这件事,技术难度其实不高,真正难的是对用户心理的把握。当你的App第一次向用户请求通知权限时,你不是在索取一个技术能力,而是在签订一份信任契约:用户允许你打断他的注意力,你就必须确保每一次打扰都值得。
所以,我给自己定下三条铁律:
- 绝不滥用高优先级:
Importance.high意味着声音、震动、横幅,只留给真正紧急的事件,比如支付失败、账号异常登录。日常的订单状态更新,用default足矣。 - 文案必须可读、可预期:通知标题要一眼看懂是什么事,不要用“您有一条新消息”这种废话。用户点进来,必须看到他预期的内容,否则就是信任崩塌。
- 提供一键关闭入口:在App设置页,必须有清晰的“通知管理”入口,让用户能随时关闭某类通知。这看似增加了工作量,实则大幅降低了用户卸载App的概率。
技术终会过时,但对用户的敬畏,永远不过时。当你把通知当作一次郑重其事的对话,而不是一次技术演示,你的App才会真正被用户放在心上。