【Flutter】flutter_local_notificationsで定期的に通知する

2024/08/26に公開

概要

定期的にアプリからローカル通知を行う実装を試してみたいと思います。

使うパッケージ

https://pub.dev/packages/flutter_local_notifications

サポートされるプラットフォーム

  • Android 4.1以上
    • NotificationCompat APIを使用しているため、古いAndroidデバイスでも動作可能らしい
  • iOS 8.0以上
    • iOSのバージョンが10より古い場合
      • UILocalNotification APIを使用
    • iOS 10以降

パッケージインストール

dependencies:
  flutter_local_notifications: ^17.1.2

Android Setup

基本的には公式ドキュメント通りに進めていきます。

  • android/app/build.gradle に以下を追加

    • Androidの古いバージョンでの後方互換性を持つスケジュール通知をサポートするためにdesugaring に依存するようになったらしい

    • desugaringとは?

      • 古いAndroidバージョンで新しいJava言語機能とAPIを使用可能にするプロセス
      • Android Gradle Plugin 4.0.0以降でサポート
      android {
        defaultConfig {
          multiDexEnabled true
        }
      
        compileOptions {
          // Flag to enable support for the new language APIs
          coreLibraryDesugaringEnabled true
          // Sets Java compatibility to Java 8
          sourceCompatibility JavaVersion.VERSION_1_8
          targetCompatibility JavaVersion.VERSION_1_8
        }
      }
      
      dependencies {
        coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
      }
      
  • android/build.gradle を以下に修正

    buildscript {
       ...
    
        dependencies {
            classpath 'com.android.tools.build:gradle:7.3.1'
            ...
        }
    
  • android/app/src/main/AndroidManifest.xml に以下を追加

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
      <application
        ...
          <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
          <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
              <intent-filter>
                  <action android:name="android.intent.action.BOOT_COMPLETED"/>
                  <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
                  <action android:name="android.intent.action.QUICKBOOT_POWERON" />
                  <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
              </intent-filter>
          </receiver>
      </application>
    

iOS Setup

  • ios/Runner/AppDelegate.swift に以下を追加

    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
    }
    

Permissionリクエスト

以下の requestPermissions を呼ぶ事で通知許可のダイアログを表示させます。

Future<void> requestPermissions() async {
  if (Platform.isIOS) {
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(
          alert: true,
          badge: true,
          sound: true,
        );
  } else if (Platform.isAndroid) {
    // Android 13 (API レベル 33) 以降で必要
    final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
        flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>();
    await androidImplementation?.requestNotificationsPermission();
  }
}

アプリがフォアグラウンドの時の処理

iOS10以降の場合、基本foregroundの通知は表示されませんが、flutter_local_notificationsではデフォルトでforeground通知を表示してくれます。

古いiOSの場合 DarwinInitializationSettingsonDidReceiveLocalNotification で処理を書く必要があります。

通知タップ時のハンドリングは onDidReceiveNotificationResponse で処理します。

ボタンをタップしたら通知を表示

まずはシンプルにボタンをタップしたら通知が表示されるように実装してみたいと思います。

以下の様なメソッドを作成し、ボタンタップしたらメソッド呼ぶようにします。

Future<void> showNotification() async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails('your channel id', 'your channel name',
          channelDescription: 'your channel description',
          importance: Importance.max,
          priority: Priority.high,
          ticker: 'ticker');
  const NotificationDetails notificationDetails =
      NotificationDetails(android: androidNotificationDetails);
  await flutterLocalNotificationsPlugin.show(
      0, 'plain title', 'plain body', notificationDetails,
      payload: 'item x');
}

実行すると↓の様に通知が表示される様になります。

iOS

image1.gif

Android

image2.gif

実装部分の詳細を見てみたいと思います。

  • AndroidNotificationDetails
    • アンドロイド特有の通知詳細
    • 必須のパラメータとしては↓の2つ
      • channelId (String)
      • channelName (String)
      • ※ Androidの通知チャンネルに関してはこちら
    • Optinalなパラメータで主なものは↓
      • channelDescription
        • チャンネルの説明
      • importance
        • Android 8.0以降で通知の重要度こちらのenum値を設定
      • priority
        • Android 7.1以下での通知の優先順位
        • こちらのenum値を設定
      • ticker
        • アクセシビリティサービスに送信される「テロップ」テキストを指定

ちなみに channelId, channelName, channelDescription は Android 8.0(API レベル 26)以降の端末だとアプリの設定画面で確認する事ができます。

image3.png

5秒後に通知を表示

次にボタンをタップして5秒後に通知が表示されるようにしてみたいと思います。

flutter_local_notifications で時間を扱う一緒にインストールされるtimezoneを使います。

最初にtimezoneパッケージの初期化を行います。Locationも Asia/Tokyo に設定しときます。

import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/standalone.dart' as tz;

void main() async{
  tz.initializeTimeZones();
  tz.setLocalLocation(tz.getLocation("Asia/Tokyo"));
  // ....
}

次に以下メソッドを作成し、ボタンがタップされたら呼ぶようにします。

Future<void> scheduleNotification() async {
  await flutterLocalNotificationsPlugin.zonedSchedule(
    0,
    'scheduled title',
    'scheduled body',
    tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
    const NotificationDetails(),
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
  );
}

↓実際に試した結果5秒後に通知がきています。

image4.gif

毎日特定の時刻に通知を表示

次に特定の時刻に通知が来るようにしたいと思います。

↓の everyScheduleNotification を呼ぶと9:00に通知が来るような実装になってます。

Future<void> everyScheduleNotification() async {
  await flutterLocalNotificationsPlugin.zonedSchedule(
    0,
    'scheduled title',
    'scheduled body',
    _scheduledDateAtHour(9),
    const NotificationDetails(),
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    matchDateTimeComponents: DateTimeComponents.time,
  );
}

tz.TZDateTime _scheduledDateAtHour(int hour) {
  final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
  tz.TZDateTime scheduledDate =
      tz.TZDateTime(tz.local, now.year, now.month, now.day, hour);
  if (scheduledDate.isBefore(now)) {
    scheduledDate = scheduledDate.add(const Duration(days: 1));
  }
  return scheduledDate;
}

void cancelAllNotifications() async {
  await flutterLocalNotificationsPlugin.cancelAll();
}

通知のキャンセルは cancelAllNotifications を呼ぶことで今回は全部の通知をキャンセルします。個別にキャンセルする場合は通知IDを保持しておいて個別にキャンセルします。

バッドノウハウ

Androidで通知時に以下エラーが出る

PlatformException (PlatformException(error, Attempt to invoke virtual method 'int java.lang.Integer.intValue()' on a null object reference, null, java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Integer.intValue()' on a null object reference

https://github.com/MaikuB/flutter_local_notifications/issues/1237#issuecomment-963835025

↑にある通り AndroidNotificationDetailsicon を設定、または AndroidInitializationSettings でデフォルトの通知アイコンを設定します。

AndroidNotificationDetails('your channel id', 'your channel name',
    channelDescription: 'your channel description',
    icon: "@mipmap/ic_launcher", // ← ここ
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker');
          
// 又は

flutterLocalNotificationsPlugin.initialize(
  const InitializationSettings(
    android: AndroidInitializationSettings('@mipmap/ic_launcher'),
  ),
);

Androidで通知設定時に以下エラーが出る

Unhandled Exception: PlatformException(exact_alarms_not_permitted, Exact alarms are not permitted, null, null)

https://stackoverflow.com/questions/76309215/unhandled-exception-platformexceptionexact-alarms-not-permitted-exact-alarms

AndroidManifest.xml に以下を追加し、

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

次に、以下のパーミッションの要求を追加します。

await androidImplementation?.requestExactAlarmsPermission();

参考URL

https://zenn.dev/flutteruniv_dev/articles/434310831e41f3

https://flutter.salon/plugin/flutter_local_notifications/

https://velog.io/@jeong_woo/Flutter-local-notification-boiler-code

Discussion