【Flutter】イベント通知の簡単な実装方法

対象者

  • モバイルアプリ開発者で、FlutterでAndroidおよびiOSプラットフォームに対応したローカル通知機能を統合したい方

はじめに

アプリのエンゲージメントとリテンションは、ユーザーとの持続的な関係を築く上で欠かせない要素です。特に、適切なタイミングでの通知は、アプリ利用の促進に直接つながります。
本記事では、Flutterのflutter_local_notificationsパッケージを活用してローカル通知を実施する方法を紹介します。AndroidとiOS、両プラットフォームの設定方法と実際のローカル通知を実施します。
他の記事を読んだときに、プッシュ通知そのものと、スケジュールの内容が混在して分かりづらかったです。今回はローカルプッシュ通知を実施するだけの手順書となります。そのためスケジュールしたい場合などは別の記事を参照してください。

flutter_local_notificationsの導入

パッケージのインストール方法

Flutterでローカル通知を簡単に実装するための第一歩は、flutter_local_notificationsパッケージをプロジェクトに追加することです。以下のコマンドを実行して、この便利なパッケージを追加しましょう。

flutter pub add flutter_local_notifications

このコマンドは、プロジェクトの依存関係にパッケージを追加し、関連する設定を自動で更新します。このステップが完了すると、Flutterアプリケーションでローカル通知の設定と管理が可能になります。

Androidの設定

Androidでflutter_local_notificationsを使用するには、いくつかの設定をbuild.gradleとAndroidManifest.xmlに追加する必要があります。これにより、アプリが適切に通知をスケジュールし、端末が再起動された後も通知が継続することを保証します。

  • android/app/build.gradle
    ビルド設定に以下の変更を加えます。これにより、Java 8のライブラリのサポートや、複数の依存関係が解決されます。
android {
-    compileSdk flutter.compileSdkVersion
+    compileSdk 34
    ndkVersion flutter.ndkVersion

    compileOptions {
+        coreLibraryDesugaringEnabled true
    }

(中略)	
        defaultConfig {
        versionName flutterVersionName
+        multiDexEnabled true
    }
    
- dependencies {}
+ dependencies {
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
+    implementation 'androidx.window:window:1.0.0'
+    implementation 'androidx.window:window-java:1.0.0'
+}
  • android/app/src/main/AndroidManifest.xml
    アプリケーションがブート完了時やアプリケーションのパッケージが置き換えられたときに通知を再スケジュールするために、以下のパーミッションとreceiver設定を追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
   <application
(中略)   
+        <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
+        <receiver android:exported="false" 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"/>
+                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
+                <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
+            </intent-filter>
+        </receiver>
    </application>

iOSの設定例

iOSでの設定は、AppDelegate.swift内で通知の権限リクエストと通知センターのデリゲートを設定することから始めます。これは、アプリケーションがフォアグラウンドまたはバックグラウンドにある場合に適切に通知を受け取るために必要です。
  • ios/Runner/AppDelegate.swift
+   if #available(iOS 10.0, *) {
+      UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
+    }

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

実装

Flutterでローカル通知機能を実装するには、flutter_local_notifications パッケージを用います。このパッケージは、AndroidとiOSの両プラットフォームに対応しており、さまざまな種類の通知を簡単に設定することが可能です。以下では、基本的な通知の設定と発火の方法について具体的なコード例を示します。

基本設定

アプリケーションの起動時に、以下のスクリプトをmain()関数内に配置して、通知機能の初期化を行います。これにより、アプリが起動する際に通知サービスが利用可能な状態になります。

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  FlutterLocalNotificationsPlugin()
    ..resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.requestNotificationsPermission()
    ..initialize(const InitializationSettings(
      android: AndroidInitializationSettings('@mipmap/ic_launcher'),
      iOS: DarwinInitializationSettings(),
    ));

  runApp(const MyApp());
}

通知の表示

アプリ内で特定のイベントが発生した時に通知をユーザーに表示するためには、以下の関数を使用します。この関数は、通知のタイトルとメッセージを引数に取り、設定に基づいてローカル通知をトリガーします。

  void showLocalNotification(String title, String message) {
    const androidNotificationDetail = AndroidNotificationDetails(
        'channel_id', // channel Id
        'channel_name' // channel Name
        );
    const iosNotificationDetail = DarwinNotificationDetails();
    const notificationDetails = NotificationDetails(
      iOS: iosNotificationDetail,
      android: androidNotificationDetail,
    );
    FlutterLocalNotificationsPlugin()
        .show(0, title, message, notificationDetails);
  }

このコードでは、まずFlutterLocalNotificationsPluginインスタンスを作成し、それを使用して通知の許可を要求し、プラットフォームごとの設定を行います。AndroidInitializationSettingsとDarwinInitializationSettingsは、それぞれAndroidとiOSの初期設定を指定します。showLocalNotification関数は、通知の内容とともにNotificationDetailsを設定し、その設定に基づいて通知を表示します。

検討事項

今回のサンプルコード作成の中で、FlutterLocalNotificationsPluginの取り扱いは、私の中で課題となりました。
サンプルコードでは、このプラグインをグローバル変数に格納して共通化して使用してました。私にはグローバル変数の使用には抵抗があります。これはプログラミングの設計哲学において、なるべくグローバルスコープを避けるという立場からです。そのため、この記事ではFlutter Local Notification Pluginをそのまま使用するアプローチを採用しています。このプラグインはシングルトンで設計されており、そのまま使用すれば共通のインスタンスを取得できます。
実際の使用としては、画面のウィジェット内でプラグインをメンバー変数として定義し、そのスコープ内で使用することで、グローバル変数としての定義を避けることができます。これにより、グローバルスコープの使用を避けつつ、各画面で必要に応じた適切な管理が可能です。

まとめ

Flutterのflutter_local_notificationsパッケージを導入する手順と設定方法を学びました。まず、パッケージをプロジェクトに追加し、AndroidとiOSの両プラットフォームで必要な設定を行いました。Androidでは、特定の権限とブート完了時のレシーバーを設定し、iOSでは通知センターのデリゲートを設定しました。実際に通知を表示するための基本的なコードも理解しました。これにより、アプリ内でユーザーへ効果的に通知を送る方法を習得しました。

参考

ソース(main.dartにコピペして動作確認用)

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  FlutterLocalNotificationsPlugin()
    ..resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.requestNotificationsPermission()
    ..initialize(const InitializationSettings(
      android: AndroidInitializationSettings('@mipmap/ic_launcher'),
      iOS: DarwinInitializationSettings(),
    ));

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyAppState();
}

class _MyAppState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ローカルプッシュ通知 テスト',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Test'),
        ),
        body: Center(
          child: FilledButton(
            onPressed: _onPressed,
            child: const Text('test'),
          ),
        ),
      ),
    );
  }

  void _onPressed() {
    showLocalNotification('Notification title', 'Notification message');
  }

  void showLocalNotification(String title, String message) {
    const androidNotificationDetail = AndroidNotificationDetails(
        'channel_id', // channel Id
        'channel_name' // channel Name
        );
    const iosNotificationDetail = DarwinNotificationDetails();
    const notificationDetails = NotificationDetails(
      iOS: iosNotificationDetail,
      android: androidNotificationDetail,
    );
    FlutterLocalNotificationsPlugin()
        .show(0, title, message, notificationDetails);
  }
}