Flutterで作成するモバイルアプリにリマインダー設定機能を実装する(ローカル通知)
はじめに
モバイルアプリにリマインダー機能が必要となったので、方法から検討する。
Push通知にはリモートとローカルがあり、今回のユースケース(リマインダを設定し通知)ではローカルで十分対応できると思われたので、ローカル通知を採用。
ただし、複数端末でアプリを利用する場合、リマインダーを設定した端末ではない端末では利用できないことが想定されるので調査する。
こちらのパッケージがメジャーなようなので採用。
iOSの設定
セットアップ
ios/AppDelegate.swift
に以下のコードを追加する。
ファイル内のどこに記述するべきかは、パッケージのサンプルプロジェクトで確認した。
import Flutter
import UIKit
@main
@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)
}
}
シンプルな通知を実装する
実装後のコード
実装後のコードは以下のようになる。
この実装で、ホーム画面のRequest Permission
ボタン押下で通知権限のリクエスト、Show plain notification with payload
ボタン押下で通知が表示される。この実装は、パッケージのサンプルコードを利用を改変したもの。
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
int id = 0;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const InitializationSettings initializationSettings = InitializationSettings(
iOS: initializationSettingsDarwin,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
runApp(
const MaterialApp(home: HomePage()),
);
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _notificationsEnabled = false;
void initState() {
super.initState();
_isIOSPermissionGranted();
}
Future<void> _isIOSPermissionGranted() async {
if (Platform.isIOS) {
final notificationsEnableOptions = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.checkPermissions();
final bool granted = notificationsEnableOptions?.isEnabled ?? false;
setState(() {
_notificationsEnabled = granted;
});
}
}
Future<void> _requestPermissions() async {
if (Platform.isIOS) {
final grantedNotificationPermission =
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
setState(() {
_notificationsEnabled = grantedNotificationPermission ?? false;
});
} else if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
final bool? grantedNotificationPermission =
await androidImplementation?.requestNotificationsPermission();
setState(() {
_notificationsEnabled = grantedNotificationPermission ?? false;
});
}
}
void _showNotification() async {
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(
presentAlert: false,
presentBadge: true,
presentSound: true,
);
const NotificationDetails notificationDetails = NotificationDetails(
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
id++, 'plain title', 'plain body', notificationDetails,
payload: 'item x');
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('example app'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
child: Text('notification enabled: $_notificationsEnabled'),
),
ElevatedButton(
onPressed: _requestPermissions,
child: const Text('Request Permission'),
),
ElevatedButton(
onPressed: _showNotification,
child: const Text('Show plain notification with payload'),
),
],
),
),
),
),
);
}
}
コード解説
以下が初期化コード。DarwinInitializationSettings
でfalseに設定しているプロパティをtrueに設定している場合は、その権限をアプリ起動時にユーザーに通知の使用許可を取ることになる。今回は、通知の使用許可を任意のボタン押下で実行したいのでfalseとした。ただし、iOS, AndroidともにUXの観点から権限が必要となったタイミングで使用許可をとる方法を推奨している。
const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const InitializationSettings initializationSettings = InitializationSettings(
iOS: initializationSettingsDarwin,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
Androidの設定
アプリケーションのGradleファイルの設定
desugarについてのリンクからJava 8+ API の desugar のサポート(Android Gradle プラグイン 4.0.0+)の内容を参考に設定する。内容を一部変更して利用している。
android {
defaultConfig {
// APIバージョンが20以下の場合必須のようなので追加せず
- multiDexEnabled true
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}
multiDexEnabled
の設定は追加していない。
以下のソースに記載の内容から不要と判断。作成したFlutterアプリケーションのflutter.minSdkVersion
が21だったため。
Java 8+ API の desugar のサポート(Android Gradle プラグイン 4.0.0+)
// Required when setting minSdkVersion to 20 or lower
Multidex support is natively included when targeting Android SDK 21 or later.
com.android.tools:desugar_jdk_libs
のバージョンについては、Gradleのバージョンに合わせて2.0.3を設定。バージョン表にある通り。
プラグインは、この機能を活用するためにAndroid Gradleプラグイン(AGP)7.3.1を使用することに注意してください。また、Android StudioはGradle 7.3以上でのみ動作するJava SDKの新しいバージョンをバンドルしているため、より高いバージョンを使用する必要があります(詳細はこちらを参照)。レガシーなapplyスクリプト構文を使うFlutterアプリの場合、これはandroid/build.gradleで指定され、主な部分は以下のようになります。
以下に、AGPのバージョンをDSLで記載する方法があり。
settings.gradle
のcom.android.application
で指定するようだ。8.1.0
に設定されていたためそのままとした。
desugar を有効にすると、Android 12L 以降で Flutter アプリがクラッシュする可能性があるという報告があります。これはプラグインの問題ではなく、Flutter 自体の問題です。考えられる解決策の 1 つは、WindowManager ライブラリを依存関係として追加することです。
ということなので、android/app/build.gradle
に以下を追加した。
どこに書くべきか分からなかったので、パッケージのexampleにあるコードを参照した。
dependencies {
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
}
また、compileSdk
が34以上に設定されている必要があるそうで、プロジェクトを確認したところそれは満たしたが、それ以下の場合の対応は不明。
リリースビルド時の設定について
手順に記載のあるgsonのリポジトリのProGuard設定ファイルをそのまま利用。
TODO: 通知アイコンなどのリソースが破棄されないようにする
Androidのマニフェストファイルについて
最低限の権限のみデフォルトで追加されている。通知のスケジュールが必要な場合は権限の追加が必要なようだ。
iOSで通知最低限の実装
通知に関する公式のドキュメント
DarwinNotificationDetails.categoryIdentifier
interruptionLevel
requestPermission.critical
alert
は非推奨。詳細は、DarwinNotificationDetailsを参照
iOSについて
スクラップが見づらくなってきたので改めてまとめる。
通知についてのiOSのドキュメントは以下
Apple Developer Documentation - Asking permission to use notifications
プラグインの初期化
プラグインの初期化コード(iOSのみ)。
final DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings();
final InitializationSettings initializationSettings = InitializationSettings(
iOS: initializationSettingsDarwin,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
プラグインの初期化に必要となるiOSとmacOSの設定項目について以降で記載する。
DarwinInitializationSettings({
bool requestAlertPermission = true,
bool requestSoundPermission = true,
bool requestBadgePermission = true,
bool requestProvisionalPermission = false,
bool requestCriticalPermission = false,
bool defaultPresentAlert = true,
bool defaultPresentSound = true,
bool defaultPresentBadge = true,
bool defaultPresentBanner = true,
bool defaultPresentList = true,
List<DarwinNotificationCategory> notificationCategories =
const <DarwinNotificationCategory>[],
});
requestAlertPermission
、requestSoundPermission
、requestBadgePermission
、requestProvisionalPermission
、requestCriticalPermission
については他のセクションで記載しているので、ここでは扱わない。
defaultが接頭辞としてつくものは、アプリがフォアグランドでの実行時に通知されたときの既定の挙動を決定する。
以下設定値についての説明
DarwinInitializationSettingsのソースコードを引用
import 'notification_category.dart';
/// iOSやmacOSなど、Darwin系オペレーティングシステム向けのプラグイン初期化設定
class DarwinInitializationSettings {
/// [DarwinInitializationSettings] のインスタンスを構築します。
const DarwinInitializationSettings({
this.requestAlertPermission = true,
this.requestSoundPermission = true,
this.requestBadgePermission = true,
this.requestProvisionalPermission = false,
this.requestCriticalPermission = false,
this.defaultPresentAlert = true,
this.defaultPresentSound = true,
this.defaultPresentBadge = true,
this.defaultPresentBanner = true,
this.defaultPresentList = true,
this.notificationCategories = const <DarwinNotificationCategory>[],
});
/// アラート表示の許可をリクエストします。
///
/// デフォルト値は true です。
final bool requestAlertPermission;
/// サウンド再生の許可をリクエストします。
///
/// デフォルト値は true です。
final bool requestSoundPermission;
/// アプリアイコンのバッジ表示許可をリクエストします。
///
/// デフォルト値は true です。
final bool requestBadgePermission;
/// iOS 12以上で暫定通知の許可をリクエストします。
///
/// Appleの特別な承認が必要です: https://developer.apple.com/documentation/usernotifications/asking_permission_to_use_notifications#3544375
///
/// デフォルト値は false です。
///
/// iOSでは、iOS 12以降に適用されます。
/// macOSでは、macOS 10.14以降に適用されます。
final bool requestProvisionalPermission;
/// 重要な通知の表示許可をリクエストします。
///
/// Appleの特別な承認が必要です: https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/
///
/// デフォルト値は false です。
final bool requestCriticalPermission;
/// 通知がアプリのフォアグラウンドでトリガーされたときにアラートを表示するかを設定します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649506-alert
///
/// デフォルト値は true です。
///
/// iOSでは、iOS 10以降、iOS 14未満で適用されます。
/// macOSでは、macOS 10.14以降、macOS 11未満で適用されます。
final bool defaultPresentAlert;
/// 通知がアプリのフォアグラウンドでトリガーされたときにサウンドを再生するかを設定します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649521-sound
///
/// デフォルト値は true です。
///
/// iOSでは、iOS 10以降に適用されます。
/// macOSでは、macOS 10.14以降に適用されます。
final bool defaultPresentSound;
/// 通知がアプリのフォアグラウンドでトリガーされたときにバッジを適用するかを設定します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649515-badge
///
/// デフォルト値は true です。
///
/// iOSでは、iOS 10以降に適用されます。
/// macOSでは、macOS 10.14以降に適用されます。
final bool defaultPresentBadge;
/// 通知がフォアグラウンドでトリガーされたときにバナー形式で表示するかを設定します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/3564812-banner
///
/// デフォルト値は true です。
///
/// iOSでは、iOS 14以降に適用されます。
/// macOSでは、macOS 11以降に適用されます。
final bool defaultPresentBanner;
/// 通知がフォアグラウンドでトリガーされたときに通知センターに表示するかを設定します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/3564813-list
///
/// デフォルト値は true です。
///
/// iOSでは、iOS 14以降に適用されます。
/// macOSでは、macOS 11以降に適用されます。
final bool defaultPresentList;
/// 利用可能な通知カテゴリ ([DarwinNotificationCategory]) を設定します。
///
/// アクション設定の変更にはアプリの再インストールまたはカテゴリ識別子の変更が必要です。
///
/// iOSでは、iOS 10以降に適用されます。
/// macOSでは、macOS 10.14以降に適用されます。
final List<DarwinNotificationCategory> notificationCategories;
}
以下は、NotificationCategory(Notification types)について。今回の実装では利用しないため深掘りしていない。
通知使用の許可
IOSFlutterLocalNotificationsPlugin#requestPermissions
でユーザーに通知使用の確認を取ることができる。以下にIOSFlutterLocalNotificationsPlugin#requestPermissions
の引数について記載する。
Future<bool?> requestPermissions({
bool sound = false,
bool alert = false,
bool badge = false,
bool provisional = false,
bool critical = false,
})
sound
、badge
は、iOSの設定にあるアプリの通知設定にある項目と対応する。
alertとは?
iOSのプッシュ通知におけるアラートが指す正確な範囲がわからないが、
バナーと通知センターに表示されるリストを指すとして理解する。
Apple Developer Documentation - alert
provisionalとは?
ユーザーから事前に通知の使用許可を得ることなく、通知を送信できる機能。ただし、サウンドやバナー表示はされず、通知センターの履歴に表示されるだけとなる。
これはユーザーから通知の使用許可を得る通常のケースでは、ユーザーはアプリからどのような通知がされるのかなどわからない状態で許可または拒否の判断をする必要があるため、必要な通知に関しても拒否してしまう可能性があるため、それを回避するための1つのユースケースであるようだ。
provisionalを有効にした場合のアプリの通知設定の一例
アプリ設定画面
「目立たない形で配信」と表示されており、provisional
が有効となっていることがわかる。
通知設定画面
通知センター
ユーザーは、実際に通知が届いてから使用許可の決定をすることができる
ユーザーが「続ける」を選択した場合
以降使用許可の選択は表示されることがないため、ユーザーは通知センターへの表示以外の機能(バッジ、バナー)も許可したい場合は、アプリの通知設定から許可する必要がある。
Apple Developer Documentation - Use provisional authorization to send trial notifications
criticalとは?
以下にあるように、災害情報など重要度の高い通知の場合設定する必要がある項目のようだ。
重大なアラートはミュート スイッチと「着信拒否」を無視します。システムは、デバイスのミュートまたは「着信拒否」設定に関係なく、重大なアラートのサウンドを再生します。カスタム サウンドと音量を指定できます。
Apple Developer Documentationより引用
通知
const DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(
subtitle: 'the subtitle',
);
const NotificationDetails notificationDetails = NotificationDetails(iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
id++,
'title of notification with a subtitle',
'body of notification with a subtitle',
notificationDetails,
payload: 'item x'
);
DarwinNotificationDetailsのソースコードを引用
import 'interruption_level.dart';
import 'notification_attachment.dart';
/// iOSやmacOSなど、Darwin系オペレーティングシステム向けの通知の詳細設定
class DarwinNotificationDetails {
/// [DarwinNotificationDetails]のインスタンスを構築します。
const DarwinNotificationDetails({
this.presentAlert,
this.presentBadge,
this.presentSound,
this.presentBanner,
this.presentList,
this.sound,
this.badgeNumber,
this.attachments,
this.subtitle,
this.threadIdentifier,
this.categoryIdentifier,
this.interruptionLevel,
});
/// 通知がアプリのフォアグラウンドでトリガーされたときにアラートを表示するかを示します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649506-alert
///
/// この値が `null` の場合、[DarwinInitializationSettings.defaultPresentAlert] のデフォルト設定が使用されます。
///
/// iOSでは、iOS 10~14に適用されます。
/// macOSでは、macOS 10.14~15に適用されます。
final bool? presentAlert;
/// 通知がアプリのフォアグラウンドでトリガーされたときにサウンドを再生するかを示します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649521-sound
///
/// この値が `null` の場合、[DarwinInitializationSettings.defaultPresentSound] のデフォルト設定が使用されます。
///
/// iOS 10以降でのみ適用されます。
final bool? presentSound;
/// 通知がアプリのフォアグラウンドでトリガーされたときにバッジ値を適用するかを示します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/1649515-badge
///
/// この値が `null` の場合、[DarwinInitializationSettings.defaultPresentBadge] のデフォルト設定が使用されます。
///
/// iOS 10以降、macOS 10.14以降でのみ適用されます。
final bool? presentBadge;
/// 通知がアプリのフォアグラウンドでトリガーされたときにバナー形式で表示するかを示します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/3564812-banner
///
/// この値が `null` の場合、[DarwinInitializationSettings.defaultPresentBanner] のデフォルト設定が使用されます。
///
/// iOS 14以降、macOS 11以降でのみ適用されます。
final bool? presentBanner;
/// 通知がアプリのフォアグラウンドでトリガーされたときに通知センターに表示するかを示します。
///
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions/3564813-list
///
/// この値が `null` の場合、[DarwinInitializationSettings.defaultPresentList] のデフォルト設定が使用されます。
///
/// iOS 14以降、macOS 11以降でのみ適用されます。
final bool? presentList;
/// 通知用に再生するファイル名を指定します。
///
/// [presentSound] が true に設定される必要があります。[presentSound] が true であり [sound] が指定されない場合、デフォルトの通知音が使用されます。
final String? sound;
/// 通知到着時にアプリアイコンのバッジに表示する数値を指定します。
///
/// `0` を指定すると現在のバッジを削除します。
/// `0` より大きい値を指定すると、その数値がバッジに表示されます。
/// `null` を指定すると、現在のバッジは変更されません。
final int? badgeNumber;
/// 通知に含まれる添付ファイルのリストを指定します。
///
/// iOS 10以降、macOS 10.14以降でのみ適用されます。
final List<DarwinNotificationAttachment>? attachments;
/// サブタイトルを指定します。
///
/// iOS 10以降、macOS 10.14以降でのみ適用されます。
final String? subtitle;
/// 通知をグループ化するために使用できるスレッド識別子を指定します。
///
/// iOS 10以降、macOS 10.14以降でのみ適用されます。
final String? threadIdentifier;
/// アプリ定義のカテゴリ識別子を指定します。
///
/// この識別子は、[InitializationSettings] を介して構成された [DarwinNotificationCategory] に対応する必要があります。
///
/// iOS 10以降、macOS 10.14以降でのみ適用されます。
final String? categoryIdentifier;
/// 通知の優先度と配信タイミングを示す割り込みレベルを指定します。
///
/// iOS 15.0 および macOS 12.0 以降でのみ適用されます。
/// Appleのドキュメント: https://developer.apple.com/documentation/usernotifications/unnotificationcontent/3747256-interruptionlevel
final InterruptionLevel? interruptionLevel;
}