flutter_local_notificationsで指定時刻にローカル通知

めざまし電話というアプリを作ろうとしたとき通知機能を使った。このプラグインではローカルで定刻に通知を出すことができる。

[アラームの後に逆モーニングコールする目覚ましアプリを作ってみた(かった)](https://qiita.com/asuyasuya/items/4f89b05d7cf77b6060d8#_reference-56c86375a957e43aaeec)
GitHub: [Stickers-hackathon/mezamashi-denwa](https://github.com/Stickers-hackathon/mezamashi-denwa/)

# 環境
Flutter 2.2.0
[flutter_local_notifications 8.2.0](https://pub.dev/packages/flutter_local_notifications)

# 準備

## インストール

```
$ flutter pub add flutter_local_notifications
```
として完了。

または、pubspec.yamlに

```
dependencies: 
  flutter_local_notifications: ^8.2.0
```
と追加して、pub getで完了。

## 権限委譲
プロジェクト名/android/app/src/main/AndroidManifest.xml に以下の行を追加する。

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.example.mezamashi_denwa"> 
   <application 
        android:label="mezamashi_denwa" 
        android:icon="@mipmap/ic_launcher"> 
        <activity 
            android:name=".MainActivity" 
            android:launchMode="singleTop" 
            android:theme="@style/LaunchTheme" 
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" 
            android:hardwareAccelerated="true" 
            android:windowSoftInputMode="adjustResize"> 
            <!-- Specifies an Android theme to apply to this Activity as soon as 
                 the Android process has started. This theme is visible to the user 
                 while the Flutter UI initializes. After that, this theme continues 
                 to determine the Window background behind the Flutter UI. --> 
            <meta-data 
              android:name="io.flutter.embedding.android.NormalTheme" 
              android:resource="@style/NormalTheme" 
              /> 
            <!-- Displays an Android View that continues showing the launch screen 
                 Drawable until Flutter paints its first frame, then this splash 
                 screen fades out. A splash screen is useful to avoid any visual 
                 gap between the end of Android's launch screen and the painting of 
                 Flutter's first frame. --> 
            <meta-data 
              android:name="io.flutter.embedding.android.SplashScreenDrawable" 
              android:resource="@drawable/launch_background" 
              /> 
            <intent-filter> 
                <action android:name="android.intent.action.MAIN"/> 
                <category android:name="android.intent.category.LAUNCHER"/> 
            </intent-filter> 
        </activity> 
        <!-- Don't delete the meta-data below. 
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> 
        <meta-data 
            android:name="flutterEmbedding" 
            android:value="2" /> 
        #追加 再起動時およびアプリケーションの更新後も通知のスケジュールを確実に維持するために必要 
        <receiver 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"/> 
            </intent-filter> 
        </receiver> 
        #追加 プラグインがスケジュールされた通知の表示を処理するために必要 
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
    </application>
    #追加 端末が起動されたときに通知を受ける権限を要求する 
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> 
    #追加 (省略可)Android通知のバイブレーションパターンをカスタマイズする場合に必要 
    <uses-permission android:name="android.permission.VIBRATE" />
</manifest>
```

## アイコン追加
通知にアイコン画像が必要なので、プロジェクト名/android/app/src/main/res/drawableに保存する。
例: https://github.com/Stickers-hackathon/mezamashi-denwa/tree/develop/android/app/src/main/res/drawable

## インポート
使いたいソースファイルで

```dart
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';         //以下3行は定刻通知に必要 
import 'package:timezone/data/latest.dart' as tz; 
import 'package:timezone/timezone.dart' as tz;
```

# コード内準備

```dart
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); 

String? selectedNotificationPayload;

Future<void> main() async { 
  // needed if you intend to initialize in the `main` function 
  WidgetsFlutterBinding.ensureInitialized();

  await _configureLocalTimeZone();

  const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); 
  final IOSInitializationSettings initializationSettingsIOS = IOSInitializationSettings(); 
  final InitializationSettings initializationSettings = InitializationSettings( 
    android: initializationSettingsAndroid, 
    iOS: initializationSettingsIOS,);

  await flutterLocalNotificationsPlugin.initialize(initializationSettings, 
      onSelectNotification: (String? payload) async { 
        if (payload != null) { 
          debugPrint('notification payload: $payload'); 
        } 
        selectedNotificationPayload = payload; 
      });

  runApp(MyApp()); 
}

Future<void> _configureLocalTimeZone() async { 
  tz.initializeTimeZones(); 
  final String? timeZoneName = await FlutterNativeTimezone.getLocalTimezone(); 
  tz.setLocalLocation(tz.getLocation(timeZoneName!)); 
}
```

# 指定時刻に通知
以下はSwitchListTileをONにしたとき午前10時にスケジュールを設定するコード

```dart
  Widget _buildTile(BuildContext context) { 
    return SwitchListTile(  
        title: Text("10:00 AM"), 
        subtitle: Text(""), 
        onChanged: (bool value) async { 
          var t = "10:00 AM"; 
          final format = DateFormat.jm(); 
          TimeOfDay schedule = TimeOfDay.fromDateTime(format.parse(t)); 
          value ? _cancelNotification(0) : _zonedScheduleNotification(schedule, 0); 
        }); 
  }



Future<void> _zonedScheduleNotification(TimeOfDay t, int i) async { // iは通知のID 同じ数字を使うと上書きされる
  tz.TZDateTime _nextInstanceOfTime() { 
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local); 
    tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, t.hour, t.minute); 
    if (scheduledDate.isBefore(now)) { 
      scheduledDate = scheduledDate.add(const Duration(days: 1)); 
    } 
    return scheduledDate; 
  }

  await flutterLocalNotificationsPlugin.zonedSchedule( 
      i, 
      'scheduled title', 
      'scheduled body', 
      _nextInstanceOfTime(), 
      const NotificationDetails( 
          android: AndroidNotificationDetails('your channel id', 
              'your channel name', 'your channel description')), 
      androidAllowWhileIdle: true, 
      uiLocalNotificationDateInterpretation: 
      UILocalNotificationDateInterpretation.absoluteTime);  
}

Future<void> _cancelNotification(int i) async { //IDを指定して通知をキャンセル
  await flutterLocalNotificationsPlugin.cancel(i); 
}
```

# 参考
[flutter_local_notifications 8.2.0](https://pub.dev/packages/flutter_local_notifications)
[FlutterでローカルからNotification](https://qiita.com/tasogarei/items/8428e0d163f2b5c2a8bf)
[Flutterでローカルな通知機能を実装するためのOSごとの前設定](https://qiita.com/1d7678174656/items/0c2f233d70e7b678e2f7)