2022.10.08更新/Flutterでローカル通知を使う方法/flutter_local_notifications 12.0.0

2022.10.08

記事の内容を更新しました。
・flutter : 2.x.x → 3.0.4
・flutter_local_notifications : 5.0.0+4 → 12.0.0

Flutterでローカル通知やる必要があって調べたのでメモ。

使うパッケージは、flutter_local_notifications

flutter_local_notifications | Flutter Package
A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform.

基本的には↓の記事通りやりつつ、ググって情報を補完しながら進めました。

Local Notifications in Flutter
Hello everyone,

OS周りの設定

Android

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.local_notifcations">

 <!-- ここから追加 -->
 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 <uses-permission android:name="android.permission.VIBRATE" />
 <!-- ここまで追加 -->

  <application
       android:label="local_notifcations"
       android:icon="@mipmap/ic_launcher">

       <!-- ここから追加 -->
       <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
       <intent-filter>
         <action android:name="android.intent.action.BOOT_COMPLETED"/>
         <!-- 2021.05.27 再起動時のローカル通知継続用設定を追加 -->
         <action android:name="android.intent.action.QUICKBOOT_POWERON" />
         <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
         <!-- 2021.05.27 -->
         
         <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
       </intent-filter>
       </receiver>
       <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" /> 
       <!-- ここまで追加 -->

       <activity
           android:name=".MainActivity"

通知アイコンを配置

置く場所は、android/app/src/main/res/drawable/app_icon.png。
(例)android/app/src/main/res/drawable/mipmap-hdpi/ic_launcher.png(72×72)をコピー、リネームして作成。

2021.05.27 追記

再起動時にローカル通知が消えてしまう問題があったので調査。
BOOT_COMPLETEDは、シャットダウン+電源オン時の設定で、再起動は、QUICKBOOT_POWERONを追加する必要があった。追加したら動いた。

・ローカル通知動作確認結果

スクリーンショット 2021-05-27 18.43.53

※2021.07.03追記
「停止状態」での通知について、手元実機「S3-SH/Android10」ではがうまくいきましたが、「Mi MIX 2S/Android10」ではうまくいかず。アプリ起動するまで通知されない挙動になってしまいます。
色々試してみましたが改善できず。Androidは端末次第であることにご留意ください。
なんとか解決したいので情報あれば教えてください。

iOS

iOS/Runner/AppDelegate.swift

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
 override func application(
   _ application: UIApplication,
   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
 ) -> Bool {
 
   /* ここから追加 */
   if #available(iOS 10.0, *) {
    UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
   }
   /* ここまで追加 */

   GeneratedPluginRegistrant.register(with: self)
   return super.application(application, didFinishLaunchingWithOptions: launchOptions)
 }
}

flutter_local_notificationsパッケージのインストール

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter
 flutter_local_notifications: ^12.0.0 # 追加

dev_dependencies:

ローカル通知の組み込み

以降、lib/main.dartを編集する。

・パーミッション取得ダイアログの表示 ※iOSのみ

_requestIOSPerssion()を呼ばなくても後述の_initializePlatformSpecifics()を呼んだタイミングで自動で出るけど、通知サウンドのパーミッションを追加したければ自前で呼ぶ必要があるみたい。

〜〜〜

// ここから追加
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// ここまで追加

〜〜〜

class _MyHomePageState extends State<MyHomePage> {
 final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
     FlutterLocalNotificationsPlugin(); // 追加

 @override
 void initState() {
   super.initState();

   // ここから追加
   if (Platform.isIOS) {
     _requestIOSPermission();
   }
   // ここまで追加
 }

 // ここから追加
 void _requestIOSPermission() {
   flutterLocalNotificationsPlugin
       .resolvePlatformSpecificImplementation<
           IOSFlutterLocalNotificationsPlugin>()
       .requestPermissions(
         alert: false,
         badge: true,
         sound: false,
       );
 }
 // ここまで追加

スクリーンショット 2021-05-21 20.30.23
スクリーンショット 2021-05-22 19.21.18

初期化〜ローカル通知を単純に表示

class _MyHomePageState extends State<MyHomePage> {
 final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
     FlutterLocalNotificationsPlugin();

 @override
 void initState() {
   super.initState();

   _requestIOSPermission();

   // ここから追加
   _initializePlatformSpecifics();
   _showNotification(); // ローカル通知を表示
     // ここまで追加
 }
 
 〜〜〜
 
 // ここから追加
 void _initializePlatformSpecifics() {
   var initializationSettingsAndroid =
       AndroidInitializationSettings('app_icon');

   var initializationSettingsIOS = DarwinInitializationSettings(
     requestAlertPermission: true,
     requestBadgePermission: true,
     requestSoundPermission: false,
     onDidReceiveLocalNotification: (id, title, body, payload) async {
       // your call back to the UI
     },
   );

   var initializationSettings = InitializationSettings(
       android: initializationSettingsAndroid, iOS: initializationSettingsIOS);

   flutterLocalNotificationsPlugin.initialize(initializationSettings,
       onDidReceiveNotificationResponse: (NotificationResponse res) {
     debugPrint('payload:${res.payload}');
   });
 }

 Future<void> _showNotification() async {
   var androidChannelSpecifics = AndroidNotificationDetails(
     'CHANNEL_ID',
     'CHANNEL_NAME',
     channelDescription: "CHANNEL_DESCRIPTION",
     importance: Importance.max,
     priority: Priority.high,
     playSound: false,
     timeoutAfter: 5000,
     styleInformation: DefaultStyleInformation(true, true),
   );

   var iosChannelSpecifics = DarwinNotificationDetails();

   var platformChannelSpecifics = NotificationDetails(
       android: androidChannelSpecifics, iOS: iosChannelSpecifics);

   await flutterLocalNotificationsPlugin.show(
     0, // Notification ID
     'Test Title', // Notification Title
     'Test Body', // Notification Body, set as null to remove the body
     platformChannelSpecifics,
     payload: 'New Payload', // Notification Payload
   );
 } 
 // ここまで追加
 
 〜〜〜

スクリーンショット 2021-05-21 21.06.09

ローカル通知を指定日時に表示

あとで通知チャンネルIDでキャンセルすることができる。
「zonedSchedule(0, …」の部分。

〜

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// ここから追加
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
// ここまで追加

〜

@override
 void initState() {
   super.initState();

   _requestIOSPermission();
   _initializePlatformSpecifics();
   //_showNotification(); // コメントアウト
   _scheduleNotification(); // 追加

   // ここから追加
   // タイムゾーンデータベースの初期化
   tz.initializeTimeZones();
   // ローカルロケーションのタイムゾーンを東京に設定
   tz.setLocalLocation(tz.getLocation("Asia/Tokyo"));
      // ここまで追加
 }
 
 // ここから追加
 Future<void> _scheduleNotification() async {
   // 5秒後
   var scheduleNotificationDateTime = DateTime.now().add(Duration(seconds: 5));

   var androidChannelSpecifics = AndroidNotificationDetails(
     'CHANNEL_ID 1',
     'CHANNEL_NAME 1',
     channelDescription: "CHANNEL_DESCRIPTION 1",
     icon: 'app_icon',
     //sound: RawResourceAndroidNotificationSound('my_sound'),
     largeIcon: DrawableResourceAndroidBitmap('app_icon'),
     enableLights: true,
     color: const Color.fromARGB(255, 255, 0, 0),
     ledColor: const Color.fromARGB(255, 255, 0, 0),
     ledOnMs: 1000,
     ledOffMs: 500,
     importance: Importance.max,
     priority: Priority.high,
     playSound: false,
     timeoutAfter: 5000,
     styleInformation: DefaultStyleInformation(true, true),
   );

   var iosChannelSpecifics = DarwinNotificationDetails(
     //sound: 'my_sound.aiff',
   );

   var platformChannelSpecifics = NotificationDetails(
     android: androidChannelSpecifics,
     iOS: iosChannelSpecifics,
   );

   await flutterLocalNotificationsPlugin.zonedSchedule(
     0,
     'Test Title',
     'Test Body',
     tz.TZDateTime.from(scheduleNotificationDateTime, tz.local), // 5秒後に表示
     platformChannelSpecifics,
     payload: 'Test Payload',
     androidAllowWhileIdle: true,
     uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
   );
 }
 // ここまで追加

スクリーンショット 2021-05-21 21.06.09

他にもいくつか紹介があったが指定日時があれば十分だったので割愛。

  • 毎日の同じ時刻
  • 指定曜日の同じ時刻
  • 繰り返し(毎分)
  • ローカル通知表示に画像を追加

詳しくは、https://itnext.io/local-notifications-in-flutter-6136235e1b51 を参照。

予約済みのローカル通知の数を取得する

 @override
 void initState() {
   super.initState();

   _requestIOSPermission();
   _initializePlatformSpecifics();
   //_showNotification(); //
   _scheduleNotification();

  // 追加
   _getPendingNotificationCount().then((value) =>
       debugPrint('getPendingNotificationCount:' + value.toString()));
 }
 
 // ここから追加
 Future<int> _getPendingNotificationCount() async {
   List<PendingNotificationRequest> p =
       await flutterLocalNotificationsPlugin.pendingNotificationRequests();
   return p.length;
 }
 // ここまで追加

スクリーンショット 2021-05-21 21.45.29

予約済みのローカル通知をキャンセルする

通知チャンネルID指定キャンセル、全キャンセルの両方が可能。
どちらの場合もローカル通知はでなくなった。

@override
 void initState() {
   super.initState();

   _requestIOSPermission();
   _initializePlatformSpecifics();
   //_showNotification(); //
   _scheduleNotification();

   _getPendingNotificationCount().then((value) =>
       debugPrint('getPendingNotificationCount:' + value.toString()));

   // 追加
   _cancelNotification().then((value) => debugPrint('cancelNotification'));
}

// ここから追加
Future<void> cancelNotification() async {
   await flutterLocalNotificationsPlugin.cancel(0);
}
Future<void> cancelAllNotification() async {
   await flutterLocalNotificationsPlugin.cancelAll();
}
// ここまで追加

スクリーンショット 2021-05-21 21.55.06

ローカル通知ダイアログをタップしたとき

初期化時に指定したonDidReceiveNotificationResponseが発火。
その際、通知をセットしたときのpayload値が渡る。

 await flutterLocalNotificationsPlugin.zonedSchedule(
     ..,
     payload: 'Test Payload',
     ..,
   );
 }
 
flutterLocalNotificationsPlugin.initialize(initializationSettings,
   onDidReceiveNotificationResponse: (NotificationResponse res) {
   debugPrint('payload:${res.payload}');
});

スクリーンショット 2021-05-22 6.47.15

ここまでで、Flutterでローカル通知は大体わかったので自分のアプリ「来期アニメ」に組み込みました。

最後に全文。

lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();
  final scheduleId = 0;
  final scheduleAddSec = 3;

  @override
  void initState() {
    super.initState();

    // 通知パーミッション許可ダイアログの表示
    if (Platform.isIOS) {
      _requestIOSPermission();
    }

    // タイムゾーンデータベースの初期化
    tz.initializeTimeZones();
    // ローカルロケーションのタイムゾーンを東京に設定
    tz.setLocalLocation(tz.getLocation("Asia/Tokyo"));

    _initializePlatformSpecifics();

    // 設定済みの通知数を取得
    _getPendingNotificationCount().then((value) {
      debugPrint('getPendingNotificationCount:$value');
    });

    // 設定済みの通知をすべてキャンセル
    _cancelAllNotification().then((value) => debugPrint('cancelNotification'));
  }

  void _requestIOSPermission() {
    flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()!
        .requestPermissions(
          alert: false,
          badge: true,
          sound: false,
        );
  }

  void _initializePlatformSpecifics() {
    const initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    var initializationSettingsIOS = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: false,
      onDidReceiveLocalNotification: (id, title, body, payload) async {},
    );

    var initializationSettings = InitializationSettings(
        android: initializationSettingsAndroid, iOS: initializationSettingsIOS);

    flutterLocalNotificationsPlugin.initialize(initializationSettings,
        onDidReceiveNotificationResponse: (NotificationResponse res) {
      debugPrint('payload:${res.payload}');
    });
  }

  Future<void> _showNotification() async {
    var androidChannelSpecifics = const AndroidNotificationDetails(
      'CHANNEL_ID',
      'CHANNEL_NAME',
      channelDescription: "CHANNEL_DESCRIPTION",
      importance: Importance.max,
      priority: Priority.high,
      playSound: false,
      timeoutAfter: 5000,
      styleInformation: DefaultStyleInformation(
        true,
        true,
      ),
    );

    var iosChannelSpecifics = const DarwinNotificationDetails();

    var platformChannelSpecifics = NotificationDetails(
      android: androidChannelSpecifics,
      iOS: iosChannelSpecifics,
    );

    await flutterLocalNotificationsPlugin.show(
      0, // Notification ID
      'Test Title', // Notification Title
      'Test Body', // Notification Body, set as null to remove the body
      platformChannelSpecifics,
      payload: 'New Payload', // Notification Payload
    );
  }

  Future<void> _scheduleNotification() async {
    var scheduleNotificationDateTime =
        DateTime.now().add(Duration(seconds: scheduleAddSec));

    var androidChannelSpecifics = const AndroidNotificationDetails(
      'CHANNEL_ID 1',
      'CHANNEL_NAME 1',
      channelDescription: "CHANNEL_DESCRIPTION 1",
      icon: 'app_icon',
      //sound: RawResourceAndroidNotificationSound('my_sound'),
      //largeIcon: DrawableResourceAndroidBitmap('app_icon'),
      enableLights: true,
      color: Color.fromARGB(255, 255, 0, 0),
      ledColor: Color.fromARGB(255, 255, 0, 0),
      ledOnMs: 1000,
      ledOffMs: 500,
      importance: Importance.max,
      priority: Priority.high,
      playSound: false,
      timeoutAfter: 10000,
      styleInformation: DefaultStyleInformation(true, true),
    );

    var iosChannelSpecifics = const DarwinNotificationDetails(
        // sound: 'my_sound.aiff',
        );

    var platformChannelSpecifics = NotificationDetails(
      android: androidChannelSpecifics,
      iOS: iosChannelSpecifics,
    );

    await flutterLocalNotificationsPlugin.zonedSchedule(
      scheduleId,
      'Test Title',
      'Test Body',
      tz.TZDateTime.from(scheduleNotificationDateTime, tz.local),
      platformChannelSpecifics,
      payload: 'Test Payload',
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }

  Future<int> _getPendingNotificationCount() async {
    List<PendingNotificationRequest> p =
        await flutterLocalNotificationsPlugin.pendingNotificationRequests();
    return p.length;
  }

  Future<void> _cancelNotification(int id) async {
    await flutterLocalNotificationsPlugin.cancel(id);
  }

  Future<void> _cancelAllNotification() async {
    await flutterLocalNotificationsPlugin.cancelAll();
  }
  // ここまで追加

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ローカルプッシュ通知テスト'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                _showNotification(); // 通知をすぐに表示
              },
              child: const Text('すぐに通知を表示'),
            ),
            ElevatedButton(
              onPressed: () {
                _scheduleNotification(); // 指定日時に通知を表示
              },
              child: Text('指定日時に通知を表示($scheduleAddSec秒後)'),
            ),
            ElevatedButton(
              onPressed: () {
                _cancelNotification(scheduleId);
              },
              child: const Text('設定済みの通知を削除'),
            ),
          ],
        ),
      ),
    );
  }
}

お役に立てたようであればぜひ自作アプリのダウンロードをお願いします。

コメント

タイトルとURLをコピーしました