Flutterでローカルな通知機能を実装するためのOSごとの前設定

## 前置き
とあるアラートアプリを作成していて、ローカルでの通知機能を実装する必要が出てきました。

通知機能に関する日本語記事が何故か少ないような気がしますが、Flutter内でやることについては[この英文記事](https://itnext.io/local-notifications-in-flutter-6136235e1b51)を追っていけば問題なく進められると思います。しかし、前提として`flutter_local_notifications`というパッケージを使うために、OSごとの固有の設定が必要になるのでそれについてまとめておきます。ですので、この記事の内容は概ね[flutter_local_notificationsのページ](https://pub.dev/packages/flutter_local_notifications)のまとめです。

もし、記事の内容が参考になれば[Twitter](https://twitter.com/1d7678174656)のフォローをお願いします。個人的に進めているアプリ開発に関連することをよくツイートします。

## 更新情報
- `flutter_local_notifications` のアップデートによって，参照していた記事の内容が古くなっていたので一部修正，加筆しました．（2021/05/23）

## 環境
[flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications)というパッケージを使用します。iOS, Android共にFlutterがサポートされている最小バージョンで動きます。注意事項についてはパッケージのページを参照してください。

## Androidの設定
（iOSの設定はAndroidの後にあります）
#### 通知アイコンとサウンドをカスタマイズする方法
今回はカスタマイズする必要がなければ飛ばします。必要のある方はパッケージのページを参考にしてください。

#### 通知のスケジューリング
アプリケーションで通知をスケジュールする機能が必要な場合は、スケジュールされた通知が`AlarmManager API`を使用していつ通知を表示するかを決定するため、端末が起動されたときに通知を受ける権限を要求する必要があります。その設定は`AndroidManifest.xml`に書きます。

```xml:アプリケーションのAndroidManifest.xml
# （コメントは削除してください）
# 端末が起動されたときに通知を受ける権限を要求する
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

# ~~~
# 再起動時およびアプリケーションの更新後も通知のスケジュールを確実に維持するために必要
<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" />

# ~~~
# (省略可)Android通知のバイブレーションパターンをカスタマイズする場合に必要
<uses-permission android:name="android.permission.VIBRATE" />
```
[AndroidManifest.xmlのサンプル](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml)。書き方がわからない時はこれのマネをしてください。

#### リリース・ビルド構成(書きかけ)
Android Gradleプラグイン3.4.0以降を利用していれば、R8というツールを使って、コードの自動圧縮行ってくれますが、その際に誤って必要な部分を削除してしまうことがあるようです。一番簡単な解決方法は、flutter build apkまたはflutter build appbundleに--no-shrinkフラグを渡してR8を無効にしておけば問題は起きないはずです（調べて切れていないので、このR8を利用しない方法は良い方法なのか、あまり自信がありません。）

R8を利用する場合は、アプリケーションのリリースビルド(デバッグ機能をつけずに単純に動かすプログラム)を作成する前に、ProGuard設定ファイルをカスタマイズする必要があります。補足しておくと、ProGuardはR8の前に利用されていた圧縮ツールで、R8の設定ファイルはProguardのルールファイルと共通化されているので、先ほど追加したコンポーネントを削除しない（キープしてくれる）よう、ProGuardの設定を追加します。

```:ProGuard設定ファイル
-keep class com.dexterous.** { *; }
```

そのあと、プラグインが使用するGSON依存関係に固有の規則も追加する必要があります。また、先に示した手順で通知アイコンやサウンドをカスタマイズしている場合は、それらのリソースも削除してしまわぬようProGuard設定に追加しておく必要があリます。

公式の説明は[こちら](https://developer.android.com/studio/build/shrink-code?hl=ja)。ProGuardについて気になる方は[この記事](https://qiita.com/boohbah/items/7372b29637d28e6d671c)も一読すると良いと思います。

## iOSの設定
iOSプロジェクトの`AppDelegate.m/AppDelegate.swift`ファイル内の`didFinishLaunchingWithOptions`メソッドに次の行を追加します。

```swift:AppDelegate.m/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)
  }
}
```

Swiftではなく、Onjective-cの場合は

```objective_c
if (@available(iOS 10.0, *)) {
  [UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}
```

`AppDelegate.swift`が気になる方は[【iOS】AppDelegate.swiftってなにしてんの？](https://qiita.com/penkopenko/items/cc8f48af792c353cd9f3)をみてください。

#### アプリケーションがフォアグラウンドにある間の通知の処理
iOSアプリケーションでは、フォアグラウンド（画面に表示されている状態）での通知は表示されません。

iOS 10以降では、表示オプションを使用して、アプリがフォアグラウンドにあるときに通知がトリガーされるタイミングの動作を制御します。iOSの古いバージョンでは、プラグインを初期化するために関数に渡されるインスタンスIOSInitializationSettingsオブジェクトを作成するときに、onDidReceiveLocalNotification引数に起動するメソッドを指定する一部としてコールバックを処理する必要があります。（古いバージョンに関することは参考記事をみてください: [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications)）


## 一旦まとめ
ここまででAndroid, iOS共にプラグインを利用するための前設定を終えました。~~ここからはFlutter内で閉じているので、あとは下記の記事の通りにやっていけば大丈夫だと思います。~~ 
[Local Notifications in Flutter
](https://itnext.io/local-notifications-in-flutter-6136235e1b51)

上の記事の内容が古くなっているため，`flutter_local_notification`のページを読んだほうが良いと思います．特にExampleのコードを読むと何をやっているかがわかりやすいです．

## タイムゾーンに関する補足（2021/05/23）
毎日ある時刻に通知を飛ばしたい場合は`showWeeklyAtDayAndTime`を使っていましたが，これが非推奨となったために現在は`zonedSchedule`を使う必要があります．zonedScheduleでは`DateTime`ではなく`TZDateTime`というタイムゾーンごとのDateTimeを使用するため，`timezone`パッケージをインポートしておく必要があります．また`timezone`パッケージにはデバイスのタイムゾーンを取得する方法がないため，`flutter_native_timezone`も追加しておきましょう．

タイムゾーンの初期化はflutterのmain()で行います．

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

Future<void> main() async {
  // ...
  await _configureLocalTimeZone();
  // ...
}

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