基于 Flutter 解决 iOS 和 Android 的通知差异
一、Flutter 通知基础概述
Flutter 作为跨平台开发框架,为开发者提供了一套统一的 UI 构建和业务逻辑编写方式。然而,在通知功能上,由于 iOS 和 Android 系统的设计理念和实现方式存在差异,开发者需要了解并处理这些不同,以提供一致的用户体验。
Flutter 中处理通知主要借助插件来实现,例如 flutter_local_notifications
插件。这个插件允许开发者在 Flutter 应用中轻松地创建和管理本地通知。本地通知是指由应用自身触发,在设备本地显示的通知,无需与服务器进行交互。它适用于提醒用户执行特定操作、告知应用内新事件等场景。
(一)flutter_local_notifications 插件基础使用
- 添加依赖
在
pubspec.yaml
文件中添加flutter_local_notifications
依赖:
dependencies:
flutter_local_notifications: ^8.4.4
然后运行 flutter pub get
下载插件。
- 初始化插件
在 Flutter 应用的入口点,通常是
main.dart
文件中进行初始化:
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
var initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
var initializationSettingsIOS = DarwinInitializationSettings();
var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
runApp(MyApp());
}
这里,AndroidInitializationSettings
中的参数是应用图标资源路径,DarwinInitializationSettings
用于 iOS 初始化设置,目前它没有需要传入的参数。InitializationSettings
则将两者组合起来进行初始化。
- 发送简单通知 在需要发送通知的地方,例如某个按钮点击事件中,可以这样发送通知:
Future<void> _showNotification() async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'your channel id',
'your channel name',
importance: Importance.max,
priority: Priority.high,
);
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
'Notification Title',
'Notification Body',
platformChannelSpecifics,
);
}
AndroidNotificationDetails
中设置了通知的频道 ID、频道名称、重要性和优先级。DarwinNotificationDetails
用于 iOS 通知设置,这里采用默认设置。show
方法的第一个参数是通知 ID,用于唯一标识通知,后面依次是标题、内容和平台相关的通知细节。
二、iOS 和 Android 通知差异剖析
(一)通知样式差异
- Android 丰富的样式支持
Android 提供了多种通知样式,如大文本样式(
BigTextStyle
)、大图片样式(BigPictureStyle
)、收件箱样式(InboxStyle
)等。这些样式能让通知展示更丰富的信息。 例如,使用大文本样式:
Future<void> _showBigTextNotification() async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'your channel id',
'your channel name',
importance: Importance.max,
priority: Priority.high,
styleInformation: BigTextStyleInformation(
'This is a very long text that will be shown in a special style in the notification. '
'It can contain multiple lines and provide more detailed information.',
htmlFormatBigText: true,
summaryText: 'Summary of the big text',
),
);
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
1,
'Big Text Notification',
'Long text in a special style',
platformChannelSpecifics,
);
}
在这个例子中,BigTextStyleInformation
用于设置大文本样式的具体内容,htmlFormatBigText
表示是否支持 HTML 格式文本,summaryText
是文本的摘要。
- iOS 简洁统一的样式 iOS 的通知样式相对较为简洁统一,主要以文本形式展示标题和内容。虽然 iOS 10 引入了通知扩展,可以自定义通知内容视图,但开发相对复杂。一般情况下,iOS 通知样式遵循系统设计规范,开发者可自定义的空间相对有限。
(二)通知频道(类别)差异
- Android 的通知频道机制 Android 8.0(API 级别 26)引入了通知频道的概念。每个应用可以创建多个通知频道,每个频道可以设置不同的重要性、声音、震动等属性。用户可以在系统设置中分别管理每个频道的通知。 例如,创建一个用于消息通知的频道和一个用于提醒通知的频道:
Future<void> _createNotificationChannels() async {
var androidNotificationChannel1 = AndroidNotificationChannel(
'message_channel_id',
'Message Channel',
importance: Importance.high,
description: 'This channel is for message notifications.',
);
var androidNotificationChannel2 = AndroidNotificationChannel(
'reminder_channel_id',
'Reminder Channel',
importance: Importance.max,
description: 'This channel is for reminder notifications.',
);
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannels([androidNotificationChannel1, androidNotificationChannel2]);
}
这里创建了两个不同的通知频道,message_channel_id
用于消息通知,reminder_channel_id
用于提醒通知,通过设置不同的重要性和描述来区分。
- iOS 的通知类别 iOS 也有类似概念,称为通知类别(Notification Categories)。但与 Android 不同,iOS 的通知类别主要用于定义用户对通知的交互行为,如滑动通知出现的操作按钮等。 首先定义通知类别:
var messageCategory = DarwinNotificationCategory(
'message_category',
actions: [
DarwinNotificationAction(
'reply',
'Reply',
options: const <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.customDismissAction,
},
);
然后在初始化时添加这个类别:
var initializationSettingsIOS = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
notificationCategories: <DarwinNotificationCategory>[messageCategory],
);
这里定义了一个 message_category
类别,其中包含一个 reply
操作按钮,点击该按钮会在前台打开应用进行回复操作。
(三)通知权限差异
- Android 的通知权限
在 Android 系统中,通知权限默认是开启的。但从 Android 13(API 级别 33)开始,应用需要在
AndroidManifest.xml
文件中声明POST_NOTIFICATIONS
权限,并在运行时请求该权限:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
在代码中请求权限:
if (Platform.isAndroid && await NotificationSettings.current.status == NotificationSettingsStatus.authorized) {
// 已授权
} else {
NotificationSettingsStatus status = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
if (status == NotificationSettingsStatus.authorized) {
// 用户授权
} else {
// 用户拒绝
}
}
- iOS 的通知权限 iOS 应用在首次运行时,系统会弹出授权弹窗,询问用户是否允许应用发送通知。开发者需要在应用启动时请求通知权限:
var initializationSettingsIOS = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse details) {
// 处理通知响应
});
这里通过 DarwinInitializationSettings
中的参数请求了弹窗、角标和声音权限。
(四)通知声音差异
- Android 的通知声音设置 在 Android 中,可以为不同的通知频道设置不同的声音。可以选择系统自带的声音,也可以使用应用内的自定义声音文件。 例如,设置一个自定义声音:
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'your channel id',
'your channel name',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound('notification_sound'),
);
这里 RawResourceAndroidNotificationSound
中的参数是自定义声音文件在 res/raw
目录下的文件名(不包含文件扩展名)。
- iOS 的通知声音设置
iOS 通知声音主要依赖系统声音,开发者可以选择系统提供的几种默认声音类型,如
DarwinNotificationSoundType.default
。如果要使用自定义声音,需要将声音文件添加到项目中,并在代码中指定:
var iOSPlatformChannelSpecifics = DarwinNotificationDetails(
sound: DarwinNotificationSound(named: 'notification_sound.caf'),
);
这里假设 notification_sound.caf
是添加到 iOS 项目中的自定义声音文件。
三、基于 Flutter 解决通知差异的策略与实践
(一)统一通知样式策略
- 通用样式设计
为了在 iOS 和 Android 上保持统一的通知样式,开发者应优先选择两者都支持的基本样式。例如,简洁的文本标题和内容展示。避免过度依赖 Android 特有的复杂样式,如大图片样式在 iOS 上没有直接对应的简洁实现方式。
在代码实现上,尽量使用
flutter_local_notifications
插件提供的通用设置,例如:
Future<void> _showCommonStyleNotification() async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'common_channel_id',
'Common Channel',
importance: Importance.max,
priority: Priority.high,
);
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
2,
'Common Style Notification',
'This is a notification with a common style for both iOS and Android.',
platformChannelSpecifics,
);
}
这样的通知在 iOS 和 Android 上都能以基本的文本形式展示,保证了一致性。
- 渐进增强样式 对于一些需要展示更多信息的场景,可以采用渐进增强的策略。在 Android 上使用其丰富的样式功能,同时在 iOS 上通过其他方式尽量提供类似的信息。 例如,对于大文本内容的展示:
Future<void> _showEnhancedTextNotification() async {
if (Platform.isAndroid) {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'enhanced_channel_id',
'Enhanced Channel',
importance: Importance.max,
priority: Priority.high,
styleInformation: BigTextStyleInformation(
'This is a long text for Android. It can be shown in a special style.',
htmlFormatBigText: true,
summaryText: 'Summary for Android',
),
);
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
3,
'Enhanced Text Notification - Android',
'Long text in Android style',
platformChannelSpecifics,
);
} else if (Platform.isIOS) {
var iOSPlatformChannelSpecifics = DarwinNotificationDetails();
var platformChannelSpecifics = NotificationDetails(iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
3,
'Enhanced Text Notification - iOS',
'This is a long text. Try to show as much as possible in iOS.',
platformChannelSpecifics,
);
}
}
在这个例子中,Android 使用大文本样式展示长文本,而 iOS 则以普通文本形式尽量展示完整内容。
(二)适配通知频道(类别)差异
- 功能映射 在 Android 上,根据不同的业务需求创建多个通知频道,然后在 iOS 上通过通知类别和操作按钮来实现类似的功能区分。 例如,Android 上有一个用于新消息通知的频道和一个用于系统提醒的频道。在 iOS 上,可以为新消息通知类别添加回复操作按钮,为系统提醒类别添加确认操作按钮,以实现类似的功能区分。
// Android 创建频道
Future<void> _createAndroidChannels() async {
var newMessageChannel = AndroidNotificationChannel(
'new_message_channel_id',
'New Message Channel',
importance: Importance.high,
description: 'For new message notifications.',
);
var systemReminderChannel = AndroidNotificationChannel(
'system_reminder_channel_id',
'System Reminder Channel',
importance: Importance.max,
description: 'For system reminder notifications.',
);
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannels([newMessageChannel, systemReminderChannel]);
}
// iOS 创建类别
var newMessageCategory = DarwinNotificationCategory(
'new_message_category',
actions: [
DarwinNotificationAction(
'reply',
'Reply',
options: const <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.customDismissAction,
},
);
var systemReminderCategory = DarwinNotificationCategory(
'system_reminder_category',
actions: [
DarwinNotificationAction(
'confirm',
'Confirm',
options: const <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.customDismissAction,
},
);
var initializationSettingsIOS = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
notificationCategories: <DarwinNotificationCategory>[newMessageCategory, systemReminderCategory],
);
- 用户设置同步
如果应用允许用户在应用内设置通知相关的偏好,如是否接收新消息通知或系统提醒通知,需要在 iOS 和 Android 上同步这些设置。
可以将这些设置存储在本地,例如使用
shared_preferences
插件。在发送通知时,根据存储的设置决定是否发送以及使用哪个频道(类别)。
import 'package:shared_preferences/shared_preferences.dart';
Future<bool> getNewMessageNotificationSetting() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getBool('new_message_notification') ?? true;
}
Future<void> _sendNotificationBasedOnSetting() async {
bool isNewMessageEnabled = await getNewMessageNotificationSetting();
if (isNewMessageEnabled) {
if (Platform.isAndroid) {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'new_message_channel_id',
'New Message Channel',
importance: Importance.high,
);
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
4,
'New Message',
'You have a new message.',
platformChannelSpecifics,
);
} else if (Platform.isIOS) {
var iOSPlatformChannelSpecifics = DarwinNotificationDetails(category: 'new_message_category');
var platformChannelSpecifics = NotificationDetails(iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
4,
'New Message',
'You have a new message.',
platformChannelSpecifics,
);
}
}
}
(三)处理通知权限差异
- 权限检查与引导 在应用启动或需要发送通知时,首先检查当前平台的通知权限状态。如果权限未授予,根据不同平台的特点引导用户授予权限。 对于 Android 13 及以上版本,当权限未授予时,引导用户到系统设置页面开启权限:
if (Platform.isAndroid && await NotificationSettings.current.status != NotificationSettingsStatus.authorized) {
if (await canLaunchUrl(Uri.parse('package:${WidgetsBinding.instance.platformDispatcher.appId}'))) {
await launchUrl(Uri.parse('package:${WidgetsBinding.instance.platformDispatcher.appId}'));
}
}
这里 canLaunchUrl
和 launchUrl
来自 url_launcher
插件,用于判断是否可以打开应用设置页面并打开该页面。
对于 iOS,当权限未授予时,可以在应用内显示提示信息,告知用户到设置中开启通知权限:
if (Platform.isIOS && await NotificationSettings.current.status != NotificationSettingsStatus.authorized) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Notification Permission'),
content: const Text('Please enable notifications in Settings.'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
- 优雅降级处理 如果用户始终拒绝通知权限,应用应进行优雅降级处理。例如,在应用内提供其他方式来提醒用户重要事件,如在应用首页显示提醒徽章或推送本地可交互的提示消息,而不是依赖系统通知。
if (await NotificationSettings.current.status != NotificationSettingsStatus.authorized) {
// 显示应用内提醒徽章
setState(() {
appBadgeCount++;
});
// 推送本地可交互提示消息
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Important Notice'),
content: const Text('Although you have disabled notifications, we want to inform you...'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
(四)平衡通知声音差异
- 系统默认声音优先
为了减少兼容性问题,优先选择系统默认的通知声音。在
flutter_local_notifications
插件中,对于 Android 和 iOS 都可以设置默认声音。
// Android 设置默认声音
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'default_sound_channel_id',
'Default Sound Channel',
importance: Importance.max,
priority: Priority.high,
sound: DefaultAndroidNotificationSound(),
);
// iOS 设置默认声音
var iOSPlatformChannelSpecifics = DarwinNotificationDetails(
sound: DarwinNotificationSoundType.default,
);
这样设置可以保证在不同平台上都能使用系统默认的合适声音,避免因自定义声音带来的格式、路径等问题。
- 自定义声音适配
如果确实需要使用自定义声音,要确保声音文件在不同平台上的格式和路径都正确设置。对于 Android,声音文件放在
res/raw
目录下;对于 iOS,声音文件添加到项目中,并在代码中正确指定文件名和路径。 同时,要考虑到不同平台对声音文件格式的支持差异。Android 支持多种格式,如.mp3
、.wav
等,而 iOS 主要支持.caf
格式。
// Android 自定义声音
if (Platform.isAndroid) {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'custom_sound_channel_id',
'Custom Sound Channel',
importance: Importance.max,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound('custom_sound'),
);
var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
5,
'Custom Sound Notification - Android',
'Playing custom sound on Android',
platformChannelSpecifics,
);
} else if (Platform.isIOS) {
var iOSPlatformChannelSpecifics = DarwinNotificationDetails(
sound: DarwinNotificationSound(named: 'custom_sound.caf'),
);
var platformChannelSpecifics = NotificationDetails(iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
5,
'Custom Sound Notification - iOS',
'Playing custom sound on iOS',
platformChannelSpecifics,
);
}
通过以上策略和实践,开发者可以在 Flutter 应用中有效地解决 iOS 和 Android 通知差异问题,提供一致且良好的用户体验。在实际开发中,还需要不断测试和优化,以确保通知功能在不同设备和系统版本上的稳定性和兼容性。