はじめに (対象読者・この記事でわかること)

この記事は、Flutterでの開発経験があり、特にFirebase Cloud Messaging (FCM) などのプッシュ通知機能を利用する際に「fromMap(dynamic message) が定義されていない」というエラーに遭遇した、あるいは遭遇する可能性がある開発者を対象としています。

この記事を読むことで、以下の点が理解できるようになります。

  • fromMap(dynamic message) が定義されていない」エラーが発生する主な原因
  • RemoteMessage オブジェクトの fromMap メソッドの役割
  • エラーを解決するための具体的なコード実装方法
  • プッシュ通知の受信処理を正しく実装するためのベストプラクティス

Firebase Cloud Messaging (FCM) を利用したプッシュ通知の受信処理は、Flutterアプリ開発において頻繁に利用される機能です。しかし、この fromMap に関するエラーは、初心者がつまずきやすいポイントの一つです。本記事では、このエラーの原因を深く掘り下げ、実践的な解決策を提示することで、皆さんの開発効率向上に貢献できれば幸いです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Dart言語の基本的な文法(クラス、メソッド、コンストラクタなど)
  • Flutterの基本的な開発環境が整っていること
  • Firebase Cloud Messaging (FCM) の基本的な概念(プロジェクト設定、firebase_messaging パッケージの導入など)

Flutterのプッシュ通知受信で「fromMap(dynamic message) が定義されていない」エラーが発生する理由

Flutterでプッシュ通知を受信・処理する際、firebase_messaging パッケージが中心的な役割を担います。このパッケージは、FCMから送信されたメッセージを RemoteMessage というオブジェクトに変換してアプリケーションに渡します。RemoteMessage クラスには、メッセージの内容(タイトル、本文、データペイロードなど)をパースするための fromMap というファクトリコンストラクタが用意されています。

この fromMap メソッドは、dynamic 型のマップ(通常はFCMから受信したJSON形式のデータ)を受け取り、それを RemoteMessage オブジェクトのプロパティにマッピングする処理を行います。

では、なぜ「fromMap(dynamic message) が定義されていない」というエラーが発生するのでしょうか?主な原因は以下の2つが考えられます。

  1. firebase_messaging パッケージのバージョン不整合または古いバージョン: firebase_messaging パッケージは、継続的にアップデートされており、APIの仕様が変更されることがあります。もし、利用しているバージョンが古すぎたり、異なるバージョン間で互換性のないコードを記述していたりすると、期待される fromMap メソッドが見つからず、このエラーが発生する可能性があります。特に、firebase_messaging のメジャーバージョンアップ(例: v3系からv4系への移行など)の際には、 breaking changes が含まれることが多いため、注意が必要です。

  2. RemoteMessage オブジェクトの生成方法の間違い: RemoteMessage オブジェクトは、通常、firebase_messaging パッケージが内部で生成・提供してくれます。しかし、開発者が意図せず、RemoteMessage クラスを独自にインスタンス化しようとしたり、fromMap メソッドを不正な引数で呼び出そうとしたりする場合に、このエラーが発生することがあります。例えば、RemoteMessage(message) のように、fromMap を直接呼び出さずにインスタンス化を試みるケースです。

これらの原因を踏まえ、次のセクションでは具体的な解決策を見ていきましょう。

「fromMap(dynamic message) が定義されていない」エラーの具体的な解決策と実装方法

このエラーを解決するためには、主に firebase_messaging パッケージのバージョン管理と、プッシュ通知受信時のコールバック処理における RemoteMessage の取り扱いを正しく行うことが重要です。

1. firebase_messaging パッケージのバージョン確認と更新

まず、プロジェクトで使用している firebase_messaging パッケージのバージョンを確認しましょう。pubspec.yaml ファイルを開き、dependencies セクションにある firebase_messaging のバージョン指定を確認してください。

Yaml
dependencies: flutter: sdk: flutter firebase_messaging: ^<最新バージョン> # ここを確認 # ... その他の依存関係

一般的には、最新の安定版を使用することが推奨されます。もし古いバージョンを使用している場合は、flutter pub upgrade firebase_messaging コマンドを実行して最新版に更新してみてください。

注意点: メジャーバージョンが大きく異なる場合(例: ^3.0.0 から ^4.0.0 へ)、breaking changes が含まれている可能性があります。その際は、firebase_messaging の公式ドキュメントや、バージョンアップに伴う変更点をまとめたリリースノートを確認し、コードの修正が必要になることがあります。

2. プッシュ通知受信時のコールバック処理における RemoteMessage の正しい取り扱い

FCMからのプッシュ通知を受信する際、firebase_messaging パッケージはいくつかのコールバックメソッドを提供しています。これらのコールバックの中で、受信したメッセージを RemoteMessage オブジェクトとして受け取ります。

2.1. アプリ起動中に通知を受信した場合 (onMessage)

アプリがフォアグラウンド(起動中)のときに通知を受信した場合、FirebaseMessaging.onMessage ストリームが発火します。このストリームからは RemoteMessage オブジェクトが直接渡されるため、fromMap を自分で呼び出す必要はありません。

Dart
import 'package:firebase_messaging/firebase_messaging.dart'; // ... // アプリ起動中に通知を受信した場合の処理 FirebaseMessaging.onMessage.listen((RemoteMessage message) { print("Got a message whilst in the foreground!"); print("Message data: ${message.data}"); if (message.notification != null) { print("Message also contained a notification: ${message.notification!.title}"); print("Message also contained a notification: ${message.notification!.body}"); } });

2.2. アプリ終了中またはバックグラウンド中に通知を受信した場合 (getInitialMessageonBackgroundMessage)

アプリがバックグラウンドまたは終了している状態で通知を受信し、その通知をタップしてアプリを起動した場合、FirebaseMessaging.getInitialMessage() でそのメッセージを取得できます。

また、バックグラウンドで通知を受信した際の処理(通知の表示やデータ処理など)は、FirebaseMessaging.onBackgroundMessage で定義します。このコールバック関数は、メインのDart isolateとは別のIsolateで実行されるため、UIの更新などは直接行えません。

onBackgroundMessage に渡されるメッセージも RemoteMessage オブジェクトです。

Dart
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; // 通知表示用 // バックグラウンドメッセージハンドラー // この関数はメインのDart Isolate外で実行されるため、 // UIの更新やnavigatorの操作などは直接行えません。 @pragma('vm:entry-point') Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async { // Dartの初期化を待つ WidgetsFlutterBinding.ensureInitialized(); // バックグラウンドでの通知処理 print("Handling a background message: ${message.messageId}"); print("Message data: ${message.data}"); // 例: ローカル通知を表示する await _showNotification(message); // 後述の_showNotification関数で実装 } // アプリ起動時にバックグラウンドハンドラーを登録 FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); // アプリ起動時に、アプリ終了中に受信したメッセージを取得 Future<void> setupInteractedMessage() async { // アプリが終了状態から起動された場合に、起動をトリガーしたメッセージを取得 RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { print("App was launched from a notification: ${initialMessage.messageId}"); // 通知をタップして起動された場合の処理 _handleMessageOpenedApp(initialMessage); // 後述の_handleMessageOpenedApp関数で実装 } // アプリがフォアグラウンドまたはバックグラウンドの時に、 // 通知をタップしてアプリが起動された場合の処理 FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpenedApp); } // 通知をタップしてアプリが起動された場合の共通処理 void _handleMessageOpenedApp(RemoteMessage message) { print("Message tapped: ${message.messageId}"); print("Message data: ${message.data}"); // ここで、通知の内容に基づいて画面遷移などの処理を行う // 例: ニュース記事IDがあれば、その記事詳細画面へ遷移 // navigatorKey.currentState?.pushNamed('/details', arguments: message.data['id']); } // ローカル通知を表示するヘルパー関数 (Flutter Local Notificationsプラグインを使用) Future<void> _showNotification(RemoteMessage message) async { const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); // アプリのアイコン名 const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await flutterLocalNotificationsPlugin.initialize(initializationSettings); final AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( 'your_channel_id', // 通知チャネルID 'Your Channel Name', // 通知チャネル名 channelDescription: 'your_channel_description', // 通知チャネルの説明 importance: Importance.max, priority: Priority.high, showWhen: false, ); final DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(); final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails); await flutterLocalNotificationsPlugin.show( 0, // 通知ID message.notification?.title ?? 'New Notification', message.notification?.body ?? 'You have received a new message.', notificationDetails, payload: message.data.toString(), // 通知タップ時のペイロード ); } // アプリケーションのエントリポイントなど、適切な場所で setupInteractedMessage() を呼び出す void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); await setupInteractedMessage(); // ここで呼び出す runApp(MyApp()); }

重要なポイント:

  • FirebaseMessaging.onMessage.listen((RemoteMessage message) { ... });
  • FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
  • FirebaseMessaging.instance.getInitialMessage()
  • FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpenedApp);

これらのコールバックに渡される message は、firebase_messaging パッケージによって既に RemoteMessage オブジェクトに変換されています。したがって、これらのコールバックの中で RemoteMessage.fromMap() を直接呼び出す必要はありません。もし、これらのコールバック内で fromMap を呼び出そうとすると、「fromMap(dynamic message) が定義されていない」というエラーに繋がる可能性が高いです。

3. dynamic 型のマップを直接 RemoteMessage に変換しようとしていないか確認

場合によっては、FCMから受信したデータペイロードを自分で Map<String, dynamic> として解析し、それを RemoteMessage.fromMap() に渡そうとしているコードがあるかもしれません。しかし、RemoteMessage.fromMap() は、FCMのネイティブSDKが生成した生データ(通常はJSON文字列をパースした Map<String, dynamic>)を引数として想定しています。

誤った例:

Dart
// 実際にはこのようなコードは不要で、エラーの原因になる可能性が高い Map<String, dynamic> receivedData = jsonDecode(someJsonString); // 例 RemoteMessage newMessage = RemoteMessage.fromMap(receivedData); // これは意図しない使い方

正しい使い方: 前述の通り、firebase_messaging パッケージが RemoteMessage オブジェクトを直接提供してくれるため、開発者が RemoteMessage.fromMap() を手動で呼び出す場面はほとんどありません。

4. iOS特有の設定確認

iOSの場合、プッシュ通知が正しく機能するために、AppDelegate.swift (または AppDelegate.m) に追加の設定が必要になる場合があります。特に、Silent Push Notification (バックグラウンドでデータのみを受け取る通知) を扱う際には、didReceiveRemoteNotification メソッドの適切な実装が重要です。

Swift
// AppDelegate.swift の例 import UIKit import Flutter import FirebaseMessaging // Import FirebaseMessaging @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Firebase Messagingの初期化 FirebaseApp.configure() // ユーザーの通知許可を要求 Messaging.messaging().tokenWithCompletion { token, error in if let error = error { print("Error fetching FCM registration token: \(error)") } else if let token = token { print("FCM registration token: \(token)") } } // iOS 10+ では UserNotifications への登録が必要 if #available(iOS 10.0, *) { // これにより、FirebaseMessaging.onMessage や onBackgroundMessage が // 正しく処理されるようになります。 UNUserNotificationCenter.current().delegate = self } GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // iOS 10+ でフォアグラウンド通知を表示する場合 @available(iOS 10.0, *) override func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { let userInfo = notification.request.content.userInfo // アプリがフォアグラウンドの場合、FirebaseMessaging.onMessage が発火します。 // ここでカスタムの通知表示を行いたい場合は、completionHandler にオプションを指定します。 // 例: バナー表示とサウンド再生 completionHandler([.banner, .sound, .badge]) } // ユーザーが通知をタップしたときに呼び出されます。 @available(iOS 10.0, *) override func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo // アプリがバックグラウンドまたは終了状態から起動された場合、 // FirebaseMessaging.onMessageOpenedApp や getInitialMessage で処理されます。 // ここでは、通知タップ時の追加処理を記述することも可能です。 completionHandler() } // バックグラウンドで通知を受信した場合 (iOS 10未満) // iOS 10以降では、UNUserNotificationCenterDelegate のメソッドで処理されます。 override func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { // SwiftでFirebase Messagingを使用している場合、 // 以下のコードで didReceiveRemoteNotification を dispatch できます。 // (ただし、通常は onBackgroundMessage で処理されます) Messaging.messaging().app(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } }

特に、UNUserNotificationCenter.current().delegate = self の設定や、willPresentdidReceive メソッドの実装が、フォアグラウンド/バックグラウンドでの通知処理に影響します。firebase_messaging パッケージのドキュメントを参考に、これらの設定が正しく行われているか確認してください。

まとめ

fromMap(dynamic message) が定義されていない」というFlutterのエラーは、主に firebase_messaging パッケージのバージョン問題、またはプッシュ通知受信時のコールバック処理において、RemoteMessage オブジェクトの扱いを誤っている場合に発生します。

  • firebase_messaging パッケージのバージョンを確認し、最新版または互換性のあるバージョンに更新しましょう。
  • FirebaseMessaging.onMessageFirebaseMessaging.onBackgroundMessageFirebaseMessaging.instance.getInitialMessage()FirebaseMessaging.onMessageOpenedApp といった、パッケージが提供するコールバックメソッド内で RemoteMessage オブジェクトを直接受け取っており、RemoteMessage.fromMap() を手動で呼び出す必要はありません。
  • iOSの場合は、AppDelegate.swift のプッシュ通知関連の設定が正しく行われているか確認してください。

これらの点に注意して実装することで、プッシュ通知の受信・処理をスムーズに行うことができるはずです。

参考資料