Flutterでローカル通知の動作を確認する/flutter_local_notifications

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。
(例)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


 # The following adds the Cupertino Icons font to your application.
 # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^1.0.2
 flutter_local_notifications: ^5.0.0+4 # 追加

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 = IOSInitializationSettings(
     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,
       onSelectNotification: (String payload) async {
     //onNotificationClick(payload); // your call back to the UI
   });
 }

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

   var iosChannelSpecifics = IOSNotificationDetails();

   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でキャンセルすることができる。
「schedule(0, …」の部分。

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

   _requestIOSPermission();
   _initializePlatformSpecifics();
   //_showNotification(); // コメントアウト
   _scheduleNotification(); // 追加
 }
 
 // ここから追加
 Future<void> _scheduleNotification() async {
   var scheduleNotificationDateTime = DateTime.now().add(Duration(seconds: 5));
   var androidChannelSpecifics = AndroidNotificationDetails(
     'CHANNEL_ID 1',
     'CHANNEL_NAME 1',
     "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 = IOSNotificationDetails(
     //sound: 'my_sound.aiff',
   );
   var platformChannelSpecifics = NotificationDetails(
     android: androidChannelSpecifics,
     iOS: iosChannelSpecifics,
   );
   await flutterLocalNotificationsPlugin.schedule(
     0,
     'Test Title',
     'Test Body',
     scheduleNotificationDateTime,
     platformChannelSpecifics,
     payload: 'Test Payload',
   );
 }
 // ここまで追加

スクリーンショット 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

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

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

 await flutterLocalNotificationsPlugin.schedule(
     0,
     'Test Title',
     'Test Body',
     scheduleNotificationDateTime,
     platformChannelSpecifics,
     payload: 'Test Payload',
   );
 }
 
 flutterLocalNotificationsPlugin.initialize(initializationSettings,
       onSelectNotification: (String payload) async {
     debugPrint(
         'flutterLocalNotificationsPlugin.initialize.' + payload.toString());
     //onNotificationClick(payload); // your call back to the UI
   });

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

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

最後に全文。

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

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

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

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}

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

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

   if (Platform.isIOS) {
     _requestIOSPermission();
   }

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

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

   //_cancelAllNotification().then((value) => debugPrint('cancelNotification'));
 }

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

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

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

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

   flutterLocalNotificationsPlugin.initialize(initializationSettings,
       onSelectNotification: (String payload) async {
     debugPrint(
         'flutterLocalNotificationsPlugin.initialize.' + payload.toString());
   });
 }

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

   var iosChannelSpecifics = IOSNotificationDetails();

   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: 2));
   var androidChannelSpecifics = AndroidNotificationDetails(
     'CHANNEL_ID 1',
     'CHANNEL_NAME 1',
     "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: 10000,
     styleInformation: DefaultStyleInformation(true, true),
   );

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

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

   await flutterLocalNotificationsPlugin.schedule(
     0,
     'Test Title',
     'Test Body',
     scheduleNotificationDateTime,
     platformChannelSpecifics,
     payload: 'Test Payload',
   );
 }

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

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

 Future<void> _cancelAllNotification() async {
   await flutterLocalNotificationsPlugin.cancelAll();
 }

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

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text('local notifications test'),
     ),
     body: Center(child: Text('test')),
   );
 }
}

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

コメント

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