[Flutter] ローカルプッシュ通知

2023-03-18 hit count image

Flutterでflutter_local_notificationsを使って端末自体から特定時間にプッシュ通知を表示する方法について説明します。

概要

今回のブログポストではflutter_local_notificationsパッケージを使ってアプリ内で特定時間にプッシュ通知を表示する方法について説明します。

このブログポストで紹介するソースコードは下記のリンクで確認できます。

Flutterプロジェクト生成

Flutterでflutter_local_notificationsの使い方を確認するため次のコマンドを実行して新いプロジェクトを生成します。

flutter create flutter_local_notifications_example

flutter_local_notificationsのインストール

flutter_local_notificationsを使うためには、次のコマンドを実行してflutter_local_notificationsと追加で必要なパッケージをインストールする必要があります。

flutter pub add flutter_local_notifications flutter_native_timezone flutter_app_badger
  • flutter_native_timezone: 特定時間にメッセージを表示するため必要なパッケージ
  • flutter_app_badger: アプリアイコンのバッジ(Badge)を初期化するため必要なパッケージ

このようにインストールしたflutter_local_notificationsを使う方法について知れべてみます。

flutter_local_notificationsのアンドロイド設定

flutter_local_notificationsをアンドロイドで使うためにはandroid/app/build.gradleファイルを開いて下記のように修正する必要があります。

...
android {
  compileSdkVersion 33

  compileOptions {
    ...
    coreLibraryDesugaringEnabled true
  }
  defaultConfig {
      ...
      multiDexEnabled true
  }
  ...
}

dependencies {
  ...
  implementation "androidx.multidex:multidex:2.0.1"
  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

例題コード

flutter_local_notificationsの使い方を確認するため./lib/main.dartファイルを開いて下記のように修正します。

import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.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;

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
    _init();
  }

  @override
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      FlutterAppBadger.removeBadge();
    }
  }

  Future<void> _init() async {
    await _configureLocalTimeZone();
    await _initializeNotification();
  }

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

  Future<void> _initializeNotification() async {
    const DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings(
      requestAlertPermission: false,
      requestBadgePermission: false,
      requestSoundPermission: false,
    );
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('ic_notification');

    const InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );
    await _flutterLocalNotificationsPlugin.initialize(initializationSettings);
  }

  Future<void> _cancelNotification() async {
    await _flutterLocalNotificationsPlugin.cancelAll();
  }

  Future<void> _requestPermissions() async {
    await _flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(
          alert: true,
          badge: true,
          sound: true,
        );
  }

  Future<void> _registerMessage({
    required int hour,
    required int minutes,
    required message,
  }) async {
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
    tz.TZDateTime scheduledDate = tz.TZDateTime(
      tz.local,
      now.year,
      now.month,
      now.day,
      hour,
      minutes,
    );

    await _flutterLocalNotificationsPlugin.zonedSchedule(
      0,
      'flutter_local_notifications',
      message,
      scheduledDate,
      NotificationDetails(
        android: AndroidNotificationDetails(
          'channel id',
          'channel name',
          importance: Importance.max,
          priority: Priority.high,
          ongoing: true,
          styleInformation: BigTextStyleInformation(message),
          icon: 'ic_notification',
        ),
        iOS: const DarwinNotificationDetails(
          badgeNumber: 1,
        ),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Notifications'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await _cancelNotification();
            await _requestPermissions();

            final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
            await _registerMessage(
              hour: now.hour,
              minutes: now.minute + 1,
              message: 'Hello, world!',
            );
          },
          child: const Text('Show Notification'),
        ),
      ),
    );
  }
}

コード分析

flutter_local_notificationsの使い方を確認するため、例題コードをもっと詳しく説明します。

バッジの初期化

flutter_local_notificationsはアプリアイコンのバッジ(Badge)を初期化するコードを提供してないです。したがって、FlutterAppBaderを使ってアプリがForegroundの状態になる時、バッジを初期化する必要があります。

...
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ...
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
    ...
  }

  @override
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      FlutterAppBadger.removeBadge();
    }
  }
  ...
}

flutter_local_notificationの初期化

flutter_local_notificationを使って特定な時間にローカルプッシュメッセージを表示するためflutter_local_notificationを次のように初期化する必要があります。

...
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  @override
  void initState() {
    super.initState();
    ...
    _init();
  }

  ...

  Future<void> _init() async {
    await _configureLocalTimeZone();
    await _initializeNotification();
  }

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

  Future<void> _initializeNotification() async {
    const DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings(
      requestAlertPermission: false,
      requestBadgePermission: false,
      requestSoundPermission: false,
    );
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('ic_notification');

    const InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );
    await _flutterLocalNotificationsPlugin.initialize(initializationSettings);
  }
  ...
}

次のコードを使って現在端末の現在時間を登録します。

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

また、次のようにiOSのメッセージ権限リクエストを初期化します。iOSの初期化時、権限リクエストのメッセージがすぐ表示されないようにするため、全ての値をfalseで設定しました。

Androidic_notificationを使ってプッシュメッセージのアイコンを設定しました。そのアイコンは./android/app/src/main/res/drawable*フォルダに保存します。

...
Future<void> _initializeNotification() async {
  const DarwinInitializationSettings initializationSettingsIOS =
      DarwinInitializationSettings(
    requestAlertPermission: false,
    requestBadgePermission: false,
    requestSoundPermission: false,
  );
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('ic_notification');

  const InitializationSettings initializationSettings =
      InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );
  await _flutterLocalNotificationsPlugin.initialize(initializationSettings);
}

メッセージ登録キャンセル

新しいメッセージを登録する時、以前登録されたメッセージを全てキャンセルするためcancelAll関数を使います。

...
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ...
  Future<void> _cancelNotification() async {
    await _flutterLocalNotificationsPlugin.cancelAll();
  }
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Notifications'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await _cancelNotification();
            ...
          },
          child: const Text('Show Notification'),
        ),
      ),
    );
  }
}

権限リクエスト

プッシュメッセージを登録する前に、iOSのプッシュメッセージ権限をリクエストします。このコードはユーザが権限リクエスト画面で権限を決めると、二度とユーザの権限をリクエストしないです。

...
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ...
  Future<void> _requestPermissions() async {
    await _flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(
          alert: true,
          badge: true,
          sound: true,
        );
  }
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Notifications'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            ...
            await _requestPermissions();

            ...
          },
          child: const Text('Show Notification'),
        ),
      ),
    );
  }
}

メッセージ登録

最後に、特定時間の1分後にメッセージが表示されるようにプッシュメッセージを登録します。このメッセージは毎日同じ時間にメッセージが表示されます。

...
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ...
  Future<void> _registerMessage({
    required int hour,
    required int minutes,
    required message,
  }) async {
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
    tz.TZDateTime scheduledDate = tz.TZDateTime(
      tz.local,
      now.year,
      now.month,
      now.day,
      hour,
      minutes,
    );

    await _flutterLocalNotificationsPlugin.zonedSchedule(
      0,
      'flutter_local_notifications',
      message,
      scheduledDate,
      NotificationDetails(
        android: AndroidNotificationDetails(
          'channel id',
          'channel name',
          importance: Importance.max,
          priority: Priority.high,
          ongoing: true,
          styleInformation: BigTextStyleInformation(message),
          icon: 'ic_notification',
        ),
        iOS: const DarwinNotificationDetails(
          badgeNumber: 1,
        ),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Notifications'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            ...
            final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
            await _registerMessage(
              hour: now.hour,
              minutes: now.minute + 1,
              message: 'Hello, world!',
            );
          },
          child: const Text('Show Notification'),
        ),
      ),
    );
  }
}

zonedScheduleのIDを同じIDに設定すると、同じIDを持ってるメッセージが現在表示されてる状態の場合、メッセージが重複で表示されないです。

...
await _flutterLocalNotificationsPlugin.zonedSchedule(
  0,
  ...
);
...

AndroidNotificationDetailsongoingtrueで設定すると、アプリを実行する時だけ、メッセージが消えるように設定することができます。

await _flutterLocalNotificationsPlugin.zonedSchedule(
  ...
  NotificationDetails(
    android: AndroidNotificationDetails(
      ...
      ongoing: true,
      ...
    ),
    ...
  ),
  ...
);

確認

このように設定したらflutter_local_notificationsが上手く動作するか確認してみましょう。シミュレータを実行して開発したアプリを実行すると、次のような画面が確認できます。

Flutter - flutter_local_notifications example

次はShow Notificationボタンを押すと、次のような権限設定画面が表示されることが確認できます。

Flutter - flutter_local_notifications permission alert

ここでAllowを選択して、メッセージを受信できるように設定します。そしてアプリをBackgroundに転換して、1分を待つと次のようにメッセージが表示されることが確認できます。

Flutter - flutter_local_notifications scheduled message

そしてアプリを実行した後、再びアプリアイコンを確認すると次のようにバッジが上手く消えてことが確認できます。

完了

これでFlutterでflutter_local_notificationsを使ってローカルで特定の時間に定期的にメッセージを送信する方法について確認しました。皆さんもこの機能を使ってプッシュメッセージをローカルから定期的に送信するアプリを開発してみてください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts