Flutterでプッシュ通知を実装(Firebaseを使わず、flutter_local_notificationsを使う)

アイキャッチ_Flutterでプッシュ通知を実装(Firebaseを使わず、flutter_local_notificationsを使う) 技術メモ

Firebaseを使わず、さくっと実装できるプッシュ通知を解説します。

サンプルアプリは以下のリポジトリを参照してください。

今回作るもの

追加する必要があるパッケージ

パッケージ用途
flutter_local_notificationsプッシュ通知を送る
flutter_native_timezone通知をスケジュールするための、タイムゾーンを取得する

iOSでの固有設定

ネイティブのコードにちょっと追記しないといけないことと、通知権限について簡単に書いておきます。

AppDelegate.swiftに追記

プロジェクトルート > ios > Runner > AppDelegate.swift に以下のように追記する。
参考:https://pub.dev/packages/flutter_local_notifications#-ios-setup

import UIKit
import Flutter

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

通知権限まわりの話

サンプルアプリでは、初期化の際に通知権限のリクエストをしているが、もしそこで拒否されれば、プッシュ通知を送ることができない。

もし、ユーザーがリマインド時間を設定するなどの、通知を望んでいるような操作をした場合は、permission_handler を使って権限を調べ、なければ設定アプリを開くよう誘導すると、UXが高いアプリになると思う。

Androidでの固有設定

AndroidはiOSより厄介です。特に、通知アイコンを自作する必要があることと、それをリリースビルド時の保持するようにkeep.xmlを作成する必要があることには注意です。

必ず、リリースビルドしたものでテストを行いましょう。

AndroidManifest.xmlに追記

プロジェクトルート > android > app > src > main > AndroidManifest.xml に以下を追記します。(既存のactivityタグに記載すればOKです)
参考:サンプルアプリのAndroidManifest.xml

<activity
    android:showWhenLocked="true"
    android:turnScreenOn="true">

これにより、通知がきたときに画面がオンになり、デバイスのロック画面でも通知が表示されるようになります。

通知アイコンの作成と設定

通知アイコンの作り方は省略しますが、作成した画像ファイルを プロジェクトルート > android > app > src > main > res > drawble に配置します。
参考:サンプルアプリの通知アイコン

リリースビルド時に通知に必要なコードが削除されないようにする

解説はT.B.D.

プロジェクトルート > android > app > proguard-rules.pro を追加します。
参考:サンプルアプリのproguard-rules.pro

リリースビルド時も通知アイコンが正しく表示されるようにする

R8コンパイラのデフォルトの動作として、リリースビルド時に、通知アイコンが破棄されてしまいます。

対処は方法、プロジェクトルート > app > src > main > res > raw > keep.xml を追加して、リリースビルド時に通知アイコンの画像ファイルを保持するようにすることです。
参考:サンプルアプリのkeep.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/*" />

通知を実装

事前設定が終わったら、やっとコードを書いていきます。

今回作るアプリの全体像

以下の3つの機能を備えたサンプルアプリです。

  • 今すぐ通知
  • 通知をスケジュール(3秒後に固定)
  • スケジュールされた通知のキャンセル

また、初期化処理と画面はmain.dartに記載して、通知に関する処理はnotifications_utils.dartに記載しました。

初期化処理

main() の中で以下を記載。

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // (通知スケジュールに使う)タイムゾーンを設定する
  tz.initializeTimeZones();
  final String? timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName!));

  // flutter_local_notificationsの初期化
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon');
  const IOSInitializationSettings initializationSettingsIOS =
      IOSInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );
  const InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );
  await FlutterLocalNotificationsPlugin().initialize(initializationSettings);

  runApp(const MyApp());
}
  • 通知をスケジュールする場合には、タイムゾーンの初期化処理を実施する
  • ‘app_icon’には作成した通知アイコン画像のファイル名(拡張子なし)を記載
  • iOSの場合で、このタイミングで通知権限の許可リクエストをするなら、IOSInitializationSettingsのそれぞれのプロパティをtrueにする
    【追記】どうやら、それぞれfalseにしてもこの時点でリクエストが飛んでしまうようだ。

画面部分

各ボタンをタップしたら、通知に関するそれぞれの処理を行うだけです。

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('flutter_local_notifications_sample'),
      ),
      body: Center(
        child: Column(
          children: [
            // 今すぐ通知
            TextButton(
              onPressed: () {
                NotificationsUtils.notifyNow();
              },
              child: const Text('今すぐ通知'),
            ),

            // 通知をスケジュール
            TextButton(
              onPressed: () {
                NotificationsUtils.scheduleNotifications(
                  DateTime.now().add(const Duration(seconds: 3)),
                );
              },
              child: const Text('通知をスケジュール'),
            ),

            // スケジュールされた通知をすべてキャンセル
            TextButton(
              onPressed: () {
                NotificationsUtils.cancelNotificationsSchedule();
              },
              child: const Text('スケジュールされた通知をすべてキャンセル'),
            ),
          ],
        ),
      ),
    );
  }
}

今すぐ通知

// 今すぐ通知する
static Future<void> notifyNow() async {
  final flnp = FlutterLocalNotificationsPlugin();
  flnp.show(
    0,
    '手動通知',
    'あなたがボタンをタップしました',
    const NotificationDetails(
      android: AndroidNotificationDetails(
        '1',
        '手動通知',
        'あなたがボタンをタップしたときに通知されます',
        importance: Importance.high,
        priority: Priority.high,
      ),
      iOS: IOSNotificationDetails(
        presentSound: true,
      ),
    ),
  );
}

スケジュール通知

// 通知をスケジュール
static Future<void> scheduleNotifications(DateTime dateTime,
    {DateTimeComponents? dateTimeComponents}) async {
  // 日時をTimeZoneを考慮した日時に変換する
  final scheduleTime = tz.TZDateTime.from(dateTime, tz.local);

  // 通知をスケジュールする
  final flnp = FlutterLocalNotificationsPlugin();
  await flnp.zonedSchedule(
    1,
    'スケジュール通知',
    'あなたがスケジュールした時間になりました',
    scheduleTime,
    const NotificationDetails(
      android: AndroidNotificationDetails(
        '2',
        'スケジュール通知',
        '設定した時刻に通知されます',
        importance: Importance.high,
        priority: Priority.high,
      ),
      iOS: IOSNotificationDetails(
        presentSound: true,
      ),
    ),
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    androidAllowWhileIdle: true,
    matchDateTimeComponents: dateTimeComponents,
  );
}

通知をキャンセルする

// 通知をキャンセル
static Future<void> cancelNotificationsSchedule() async {
  final flnp = FlutterLocalNotificationsPlugin();
  await flnp.cancelAll();
}

やり残し?

  • アプリがフォアグラウンドにある場合の通知の制御(フォアグラウンドにある場合に通知は必要ないが、してしまっているなど)
  • カスタムサウンド
  • ペイロード
  • などなど

最低限は動くので、これを基に必要なものを追加していくイメージがいいと思う。

サンプルコード

コメント

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