MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于 Flutter 解决 iOS 和 Android 的通知差异

2021-12-043.9k 阅读

一、Flutter 通知基础概述

Flutter 作为跨平台开发框架,为开发者提供了一套统一的 UI 构建和业务逻辑编写方式。然而,在通知功能上,由于 iOS 和 Android 系统的设计理念和实现方式存在差异,开发者需要了解并处理这些不同,以提供一致的用户体验。

Flutter 中处理通知主要借助插件来实现,例如 flutter_local_notifications 插件。这个插件允许开发者在 Flutter 应用中轻松地创建和管理本地通知。本地通知是指由应用自身触发,在设备本地显示的通知,无需与服务器进行交互。它适用于提醒用户执行特定操作、告知应用内新事件等场景。

(一)flutter_local_notifications 插件基础使用

  1. 添加依赖pubspec.yaml 文件中添加 flutter_local_notifications 依赖:
dependencies:
  flutter_local_notifications: ^8.4.4

然后运行 flutter pub get 下载插件。

  1. 初始化插件 在 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 则将两者组合起来进行初始化。

  1. 发送简单通知 在需要发送通知的地方,例如某个按钮点击事件中,可以这样发送通知:
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 通知差异剖析

(一)通知样式差异

  1. 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 是文本的摘要。

  1. iOS 简洁统一的样式 iOS 的通知样式相对较为简洁统一,主要以文本形式展示标题和内容。虽然 iOS 10 引入了通知扩展,可以自定义通知内容视图,但开发相对复杂。一般情况下,iOS 通知样式遵循系统设计规范,开发者可自定义的空间相对有限。

(二)通知频道(类别)差异

  1. 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 用于提醒通知,通过设置不同的重要性和描述来区分。

  1. 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 操作按钮,点击该按钮会在前台打开应用进行回复操作。

(三)通知权限差异

  1. 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 {
    // 用户拒绝
  }
}
  1. iOS 的通知权限 iOS 应用在首次运行时,系统会弹出授权弹窗,询问用户是否允许应用发送通知。开发者需要在应用启动时请求通知权限:
var initializationSettingsIOS = DarwinInitializationSettings(
  requestAlertPermission: true,
  requestBadgePermission: true,
  requestSoundPermission: true,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse details) {
  // 处理通知响应
});

这里通过 DarwinInitializationSettings 中的参数请求了弹窗、角标和声音权限。

(四)通知声音差异

  1. Android 的通知声音设置 在 Android 中,可以为不同的通知频道设置不同的声音。可以选择系统自带的声音,也可以使用应用内的自定义声音文件。 例如,设置一个自定义声音:
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
  'your channel id',
  'your channel name',
  importance: Importance.max,
  priority: Priority.high,
  sound: RawResourceAndroidNotificationSound('notification_sound'),
);

这里 RawResourceAndroidNotificationSound 中的参数是自定义声音文件在 res/raw 目录下的文件名(不包含文件扩展名)。

  1. iOS 的通知声音设置 iOS 通知声音主要依赖系统声音,开发者可以选择系统提供的几种默认声音类型,如 DarwinNotificationSoundType.default。如果要使用自定义声音,需要将声音文件添加到项目中,并在代码中指定:
var iOSPlatformChannelSpecifics = DarwinNotificationDetails(
  sound: DarwinNotificationSound(named: 'notification_sound.caf'),
);

这里假设 notification_sound.caf 是添加到 iOS 项目中的自定义声音文件。

三、基于 Flutter 解决通知差异的策略与实践

(一)统一通知样式策略

  1. 通用样式设计 为了在 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 上都能以基本的文本形式展示,保证了一致性。

  1. 渐进增强样式 对于一些需要展示更多信息的场景,可以采用渐进增强的策略。在 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 则以普通文本形式尽量展示完整内容。

(二)适配通知频道(类别)差异

  1. 功能映射 在 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],
);
  1. 用户设置同步 如果应用允许用户在应用内设置通知相关的偏好,如是否接收新消息通知或系统提醒通知,需要在 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,
      );
    }
  }
}

(三)处理通知权限差异

  1. 权限检查与引导 在应用启动或需要发送通知时,首先检查当前平台的通知权限状态。如果权限未授予,根据不同平台的特点引导用户授予权限。 对于 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}'));
  }
}

这里 canLaunchUrllaunchUrl 来自 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();
            },
          ),
        ],
      );
    },
  );
}
  1. 优雅降级处理 如果用户始终拒绝通知权限,应用应进行优雅降级处理。例如,在应用内提供其他方式来提醒用户重要事件,如在应用首页显示提醒徽章或推送本地可交互的提示消息,而不是依赖系统通知。
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();
            },
          ),
        ],
      );
    },
  );
}

(四)平衡通知声音差异

  1. 系统默认声音优先 为了减少兼容性问题,优先选择系统默认的通知声音。在 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,
);

这样设置可以保证在不同平台上都能使用系统默认的合适声音,避免因自定义声音带来的格式、路径等问题。

  1. 自定义声音适配 如果确实需要使用自定义声音,要确保声音文件在不同平台上的格式和路径都正确设置。对于 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 通知差异问题,提供一致且良好的用户体验。在实际开发中,还需要不断测试和优化,以确保通知功能在不同设备和系统版本上的稳定性和兼容性。