Flutterでのバックグラウンド処理のスケジューリング実装が難しいというお話

## 初めに
iPhone純正アラームのような規定時刻&曜日に通知を送って音を鳴らす目覚ましアラームアプリを、Flutterにて個人開発しています。
しかし、作り始めてみたところ思った以上に実装難易度が高く、調べた中で得た知見を共有します。
バックグラウンドって何度も書くのアレなので以降**BG**とします。

また、いやいやこれでいけるんじゃないですか？みたいなコメントは大歓迎です。

## 追記

### 2022/01/23
ちょっとだけ光が見え始めました。
https://github.com/AyumiSashitani/demo_alarm_manager
forkした状態ではありますが、MethodChannelを用いて各OSでAlarmを実装しています。
ただ、バックグラウンド状態でしか通知が届かないので少しテコ入れをする予定ですが、この方式を用いればアラームアプリが実装できそうかも…？



## TL;DR （忙しい人用）
 - 現状アプリkill状態でのBG処理のスケジューリング実装は**MethodChannel**を用いてOS毎にコードを書く必要がありそう…?（調査中）
    - 調査段階の中途半端な状態で投稿すいません。
 - アラームアプリ等、BG処理のスケジューリングは**茨の道**。
 - 規定時刻&曜日のローカル通知は[flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications)の[zonedSchedule](https://pub.dev/packages/flutter_local_notifications#scheduling-a-notification)でOK
 - flutter_local_notificationsでは通知が来たこと自体を受け取るリスナーがない。（通知をタップすれば検知可能）
 - Androidは[android_alarm_manager_plus](https://pub.dev/packages/android_alarm_manager_plus)でBG処理&スケジューリング処理サポート
 - iOSは[background_fetch](https://pub.dev/packages/background_fetch)や[workmanager](https://pub.dev/packages/workmanager)等でBG処理自体はサポート。ただ、BG処理スケジューリング対応は厳しく3年前から状況はあまり変わっていない。

## アプリ仕様イメージ
 1 **iPhone純正アラームのようなアプリ(以下iOS標準アプリ)**
![iphone-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410187/d3303694-57e2-c9a4-f44b-e362465aceef.png)

 2 **曜日&時刻を指定可能(以下iOS標準アプリ)**
![iPhone-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410187/df82e2a8-eb61-d744-dff9-1ff5bd08df16.png)

 3 **規定時刻になるとユーザが指定した音を鳴らす（アプリkill or backgroundでも実行）**

## 調べてわかったこと

### 1.　特定の曜日&時刻を毎週通知するローカル通知は[flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications)でOK
https://pub.dev/packages/flutter_local_notifications

まずアラームをユーザに通知すべく、ローカル通知の実装を探して本ライブラリを採択。
ローカル通知を送ること自体は問題ないのですが規定曜日&規定時刻&毎週通知を送るための実装に四苦八苦しました。
結論[zonedSchedule](https://pub.dev/packages/flutter_local_notifications#scheduling-a-notification)で解決しました。

```dart 
// 実装イメージ
final flnp = FlutterLocalNotificationsPlugin();
const initialSetting = InitializationSettings(
  iOS: IOSInitializationSettings(),
  android: AndroidInitializationSettings('@mipmap/ic_launcher'),);
flnp.initialize(
        initialSetting,
        // 通知が来て、通知バーを押下した時の挙動
        onSelectNotification: (String? payload) async {
          await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return Screen();
              },
            ),
          );
        },
).then((_) async {
await flnp.zonedSchedule(
    id, // id設定はユニークであるべき。
    'タイトル',
    'メッセージ',
    // 設定したい曜日&時刻を設定（仮）
    _nextInstance(hour, min),
    ),
    // この設定を入れることによって毎週同じ曜日+時間を繰り返してくれる。
    matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime);
});

  // 参考程度に。
  tz.TZDateTime _nextInstance(int hour, int minute) {
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
    // 設定したい曜日&時刻を設定。
    // 曜日は次の曜日までのcount数(int)を設定。
    // 例)今日1/1(水)。木曜日設定であれば次は1/2(木)。なのでcountは+1
    tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day + <次曜日のcount(int)>, hour, minute);
    return scheduledDate;
  }
```
showWeeklyAtDayAndTimeという関数もライブラリ内に準備されていますが、非推奨なので使わないようにしましょう。

[公式doc引用](https://pub.dev/documentation/flutter_local_notifications/latest/)
>The schedule, showDailyAtTime and showWeeklyAtDayAndTime methods that were implemented before macOS support was added and have been marked as deprecated aren't implemented on macOS.


### 2. flutter_local_notificationsは通知を受け取るリスナーのようなものがない
1 にて規定曜日&規定時間&毎週ローカル通知送ることができました。
じゃあ、後はそのタイミングに意図したアラーム音を鳴らすような実装書けばOKじゃん！
って思いましたが、**通知が来たこと自体を受け取るリスナーがライブラリ内に無い**という大きな落とし穴がありました。

例えばFCMの場合だと[FirebaseMessaging.onBackgroundMessage](https://firebase.flutter.dev/docs/messaging/usage/)でアプリがkill or backgroundの状態でも通知を受け取った後の処理のハンドリングは可能ですよね。
※ただFCMは経験則ではありますが、時間通りキッチリ届く印象がなく正確性に欠けると思いFCMでのアラーム通知は避けました。


同様のリスナーがあればいいなと思いましたが、flutter_local_notificationsには無い模様。
`onSelectNotification`が用意されてはいますが、こちらは名前の通り届いた通知をタップしないと検知できません。

ライブラリでのissueでも議論がありました。

https://github.com/MaikuB/flutter_local_notifications/issues/1201

>No plans to at the moment but community contributions are welcome. Even then, it doesn't sound like it would support your case as iOS only supports getting callbacks about when a notification about to shown when the app is in the foreground. It wouldn't work when the app is in the background or not running

目覚ましアプリなので、自前のアラーム音再生処理をローカル通知と同時に実行したかったのですがflutter_local_notificationsでは厳しいみたいです。
よって同ライブラリはローカル通知としてのみ利用し、アラーム音の再生等BG処理は別ライブラリで対応するよう方針を変えました。

### 3. AndroidのBG処理スケジューリングは[android_alarm_manager_plus](https://pub.dev/packages/android_alarm_manager_plus)で問題無さそう。
AndroidのBG処理スケジューリングは[android_alarm_manager_plus](https://pub.dev/packages/android_alarm_manager_plus)で対応できそうです。

恐らく1分毎にチェック処理を走らせて、DB内のアラーム時刻より現在時刻を超えていたらアラーム音再生などのdart処理を実行するイメージ。
動作確認までは至っておらず本当に１分間隔で動いてくれるのかと、電池消費量が心配ではありますが公式doc等を参照する限り対応できそうです。

**公式引用**

```dart
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';

static void printHello() {
  final DateTime now = DateTime.now();
  final int isolateId = Isolate.current.hashCode;
  print("[$now] Hello, world! isolate=${isolateId} function='$printHello'");
}

main() async {
  // Be sure to add this line if initialize() call happens before runApp()
  WidgetsFlutterBinding.ensureInitialized();

  await AndroidAlarmManager.initialize();
  runApp(...);
  final int helloAlarmID = 0;
　　　　// 1分毎にアラームの設定時刻を超えたかチェック。超えていれば処理実行。（アラーム音再生等）
  await AndroidAlarmManager.periodic(const Duration(minutes: 1), helloAlarmID, printHello);
}
```

### 4. iOSでのBG処理の（正確な）スケジューリングは現状サポートされていない？
iOSのBG処理スケジューリング実装に関しては以下2つのライブラリが候補にあがりましたが、結論ボツとしました。

#### workManager
https://pub.dev/packages/workmanager


iOS側の制限により最低**15分間隔**でしか実行してくれず、
調べた限りアプリの使用頻度等によって間隔が変わり、**１５分〜無制限**でBG処理を実行してくれる模様。

```dart
// Periodic task registration
Workmanager().registerPeriodicTask(
    "2", 
    "simplePeriodicTask", 
    // When no frequency is provided the default 15 minutes is set.
    // Minimum frequency is 15 min. Android will automatically change your frequency to 15 min if you have configured a lower frequency.
    If you have configured a lower frequency. frequency: Duration(hours: 1),
)
```

#### background_fetch
https://pub.dev/packages/background_fetch

workManagerと同上。

**公式引用**
>Background Fetch is a very simple plugin which will awaken an app in the background about every 15 minutes, providing a short period of background running-time. This plugin will execute your provided callbackFn whenever a background-fetch event occurs.


### 5. アラームアプリのようなものを作りたければMethodChannelでしか実装が厳しい？
上記経緯でiOSがボトルネックとなり、
MethodChannelを用いて実装していくしかFlutterでアラームアプリは対応できないかと現状考えています…。
iOSで以下のようなアラームアプリがリリースされているのでiOS単体での実装は可能なはず。

https://apps.apple.com/jp/app/alarmy-alarm-clock/id1163786766?l=en

やりようとしては無音を流し続けてアプリを動かし続けるみたいな方法も1つの手かもしれませんが、恐らくリジェクトされる気がします。なので避けるのが吉。

https://iphone-mania.jp/news-89359/

現状のBG処理のスケジューリング実装イメージ
 - **Android**：android_alarm_manager_plus
 - **iOS**: MethodChannel(調査中)

----
### 最後に

３年前のこちらの記事でも記載されていますが、まだ公式サポートされていないようですね。

>このissue（この記事の約1ヶ月前）にもある通り、flutterでは現在、両プラットフォームにおいてバックグランド実行できる機能が制限されています。この原因として、iOSの場合は特にアプリを放置しておくと勝手に終了されてしまうため、アプリの裏側でタイマーを起動しても時間が経つとなかったことにされてしまいます。androidの場合はandroid_alarm_managerというアプリが公式で出されているのでこれを使えばよいのですが、iOSは未だに公式サポートがありません。

https://qiita.com/sh-tatsuno/items/c6a1f3e415bc3996fdf3

また果敢にもアラームアプリを作成されようとした別の方も、
同様の事象に遭遇されていました。

>アラームや連絡先の取得、電話をかける機能などはできたものの、バックグラウンドの状態から電話をかけるのが僕たちの技術ではどうにも実装することができず、ここで断念することになりました。


https://qiita.com/asuyasuya/items/4f89b05d7cf77b6060d8

Githubに類似アプリがありAPKを落としてみましたが、BG or アプリkill時では動作しませんでした。


https://github.com/GiddyNaya/clockee


また、他に何か方法が無いかと気になって、stack Overflowでも質問を投げてみました。

https://stackoverflow.com/questions/70436554/flutter-how-to-run-a-process-at-a-specific-time-even-if-the-app-is-in-backgrou

改めて調査途中段階での投稿で恐縮ではありますが、進展等あればまた追記します。
少しでもお役に立てば幸いです。