Open9

flutter_hooksでpush通知を送る

JboyHashimotoJboyHashimoto

やること

以前書いた記事を参考にする。
https://zenn.dev/flutteruniv_dev/articles/9941d106d4e211#typescriptを使う場合

設定をしたmain.dart

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:widget_cook/firebase_fcm/fcm_app.dart';
import 'package:widget_cook/firebase_options.dart';

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print("バックグラウンドメッセージの処理: ${message.messageId}");
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  FirebaseMessaging messaging = FirebaseMessaging.instance;
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  NotificationSettings settings = await messaging.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false,
    sound: true,
  );

  runApp(const FcmApp());
}

通知を受け取るflutter_hooksのコード

fcm_app.dart
import 'package:badges/badges.dart' as badges;
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:widget_cook/firebase_fcm/save_notification.dart';

class FcmApp extends HookWidget {
  const FcmApp({Key? key}) : super(key: key);

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

class MyHomePage extends HookWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  Widget build(BuildContext context) {
    final _counter = useState(0);
    final _notificationCount = useState(0);
    final _notificationReceived = useState(false);
    final _notificationMessage = useState<String?>(null);

    useEffect(() {
      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}');
          _notificationReceived.value = true;
          _notificationMessage.value = message.notification!.body;
          _notificationCount.value++; // Increase count when notification received
        }
      });
      return () {}; // Clean up
    }, const []);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: [
          badges.Badge(
            badgeContent: Text(
              '${_notificationCount.value}', // Display notification count
              style: TextStyle(color: Colors.white),
            ),
            position: BadgePosition.topEnd(top: 0, end: 3),
            child: IconButton(
              icon: Icon(Icons.notifications),
              onPressed: () {
                _notificationReceived.value = false;
              },
            ),
          )
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${_counter.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
            if (_notificationMessage.value != null)
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text(
                  'Notification message: ${_notificationMessage.value}',
                  style: TextStyle(fontSize: 20, color: Colors.red),
                ),
              ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => const SaveNotificationPage()),
          );
        },
        tooltip: 'Save Notification',
        child: const Icon(Icons.add),
      ),
    );
  }
}

通知に必要なfcmトークンを保存する機能

save_notification.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class SaveNotificationPage extends HookWidget {
  const SaveNotificationPage({super.key});

  
  Widget build(BuildContext context) {
    final _formKey = useMemoized(() => GlobalKey<FormState>());
    final _titleController = useTextEditingController();
    final _bodyController = useTextEditingController();

    Future<void> _saveNotification() async {
      if (_formKey.currentState!.validate()) {
        final deviceToken = await FirebaseMessaging.instance.getToken();
        CollectionReference notifications = FirebaseFirestore.instance.collection('notifications');

        await notifications.add({
          'title': _titleController.text,
          'body': _bodyController.text,
          'token': deviceToken,
        });

        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Notification saved!')),
        );

        _titleController.clear();
        _bodyController.clear();
      }
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('Save Notification'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(labelText: 'Title'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a title';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _bodyController,
                decoration: InputDecoration(labelText: 'Body'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a body';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16.0),
              ElevatedButton(
                onPressed: _saveNotification,
                child: Text('Save Notification'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
JboyHashimotoJboyHashimoto

通知の数を消したい時

消すというよりは、リセットする感じですね。0の数値を渡すだけで実現できます。

import 'package:badges/badges.dart' as badges;
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:widget_cook/firebase_fcm/save_notification.dart';

class FcmApp extends HookWidget {
  const FcmApp({Key? key}) : super(key: key);

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

class MyHomePage extends HookWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  Widget build(BuildContext context) {
    final _counter = useState(0);
    final _notificationCount = useState(0);
    final _notificationReceived = useState(false);
    final _notificationMessage = useState<String?>(null);

    useEffect(() {
      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}');
          _notificationReceived.value = true;
          _notificationMessage.value = message.notification!.body;
          _notificationCount
              .value++; // Increase count when notification received
        }
      });
      return () {}; // Clean up
    }, const []);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        actions: [
          // New reset button to reset badge count
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () {
              _notificationCount.value = 0;
            },
          ),
          badges.Badge(
            badgeContent: Text(
              '${_notificationCount.value}', // Display notification count
              style: TextStyle(color: Colors.white),
            ),
            position: BadgePosition.topEnd(top: 0, end: 3),
            child: IconButton(
              icon: Icon(Icons.notifications),
              onPressed: () {
                _notificationReceived.value = false;
              },
            ),
          )
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${_counter.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
            if (_notificationMessage.value != null)
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text(
                  'Notification message: ${_notificationMessage.value}',
                  style: TextStyle(fontSize: 20, color: Colors.red),
                ),
              ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => const SaveNotificationPage()),
          );
        },
        tooltip: 'Save Notification',
        child: const Icon(Icons.add),
      ),
    );
  }
}
JboyHashimotoJboyHashimoto

FCMの内部実装を見てみた。

英語わからないので、日本に翻訳する。

// ignore_for_file: require_trailing_commas
// Copyright 2020, the Chromium project authors.  詳細は AUTHORS ファイル
// ファイルを参照してください。無断複写・転載を禁じます。このソースコードの使用には、 // BSDスタイルのライセンスが適用されます。
// LICENSE ファイルを参照してください。

firebase_messaging の一部です;

/// [FirebaseMessaging] のエントリポイント。
///
/// 新しいインスタンスを取得するには、[FirebaseMessaging.instance] を呼び出します。
class FirebaseMessaging extends FirebasePluginPlatform { // キャッシュされ、遅延ロードされます。
  // [FirebaseMessagingPlatform] のインスタンスがキャッシュされ、遅延ロードされます。
  // MethodChannelFirebaseMessaging]のインスタンスをキャッシュし、遅延ロードします。
  // インスタンスを作成することを避けるためです。
  FirebaseMessagingPlatform? _delegatePackingProperty;

  static Map<String, FirebaseMessaging> _firebaseMessagingInstances = {}FirebaseMessagingPlatformを取得する _delegate { $delegatePackingProperty?
    return _delegatePackingProperty ??= FirebaseMessagingPlatform.instanceFor(
        app: app, pluginConstants: pluginConstants)}

  /// 現在の[FirebaseMessaging]インスタンスの[FirebaseApp]。
  FirebaseApp app;

  FirebaseMessaging._({必須 this.app})
      : super(app.name, 'plugins.flutter.io/firebase_messaging')/// デフォルトの[FirebaseApp]を使ってインスタンスを返す。
  static FirebaseMessaging インスタンス取得 {
    FirebaseApp defaultAppInstance = Firebase.app()return FirebaseMessaging._instanceFor(app: defaultAppInstance)}

  // Messaging はまだ複数の Firebase Apps をサポートしていません。デフォルトアプリのみ。
  /// 指定した [FirebaseApp] を使用したインスタンスを返します。
  factory FirebaseMessaging._instanceFor({必須 FirebaseApp app}) { // FirebaseMessaging._instanceFor({必須 FirebaseApp app})
    return _firebaseMessagingInstances.putIfAbsent(app.name, () {)
      return FirebaseMessaging._(app: app)});
  }

/// Flutterインスタンスがフォアグラウンドにいる間にFCMペイロードを受信したときに呼び出されるストリームを返します。
  /// Flutterインスタンスがフォアグラウンドにあるときに呼び出されるストリームを返す。
  ///
  /// Streamは[RemoteMessage]を含む。
  ///
  /// アプリがバックグラウンドにいる間、または終了している間にメッセージを処理するには、 /// [onBackgroundMessage]を参照してください、
  /// onBackgroundMessage] を参照してください。
  static Stream<RemoteMessage> get onMessage => を参照してください。
      FirebaseMessagingPlatform.onMessage.stream;

  /// 表示された通知メッセージをユーザーが押したときに呼び出される[Stream]を返します。
  /// ストリーム]を返します。
  ///
  アプリがバックグラウンド状態から開かれた場合 /// Streamイベントが送信されます。
  /// 終了していない)場合に送信されます。
  ///
  アプリが終了している間に通知によってアプリが開かれた場合、 /// [getInitialMessage]を参照してください、
  /// [getInitialMessage]を参照してください。
  static Stream<RemoteMessage> get onMessageOpenedApp => を参照してください。
      FirebaseMessagingPlatform.onMessageOpenedApp.stream;

  // 無視: use_setters_to_change_properties
  /// アプリがバックグラウンドにいる時や終了した時に呼び出されるメッセージハンドラ関数を設定します。
  /// バックグラウンドにいるか終了したときに呼び出されるメッセージハンドラ関数を設定します。
  ///
  /// このハンドラはトップレベルの関数でなければなりません。
  /// さもなければ [ArgumentError] がスローされます。
  // さもなければ [ArgumentError] が投げられる。
  static void onBackgroundMessage(BackgroundMessageHandler handler) { // この関数を呼び出すには、以下のようにします。
    FirebaseMessagingPlatform.onBackgroundMessage = handler;
  }

  /// メッセージングの自動初期化がデバイスに対して有効か無効かを返します。
  bool isAutoInitEnabled { を取得します。
    return _delegate.isAutoInitEnabled;
  }

  /// もし
/// iOS/MacOSでは、ユーザーのAPNトークンを取得することができる。
  ///
  /// iOS/MacOSデバイスにFCMサービスを使わずにメッセージを送信したい場合、これは必要です。
  /// これは、FCMサービスを使わずにiOS/MacOSデバイスにメッセージを送信したい場合に必要です。
  ///
  /// AndroidとWebでは、これは `null` を返す。
  Future<String?
    return _delegate.getAPNSToken()}

  /// このデバイスのデフォルトのFCMトークンを返します。
  ///
  /// ウェブでは、[vapidKey]が必要です。
  Future<String?> getToken({
    vapidKey、
  }) {
    return _delegate.getToken(
      vapidKey: vapidKey、
    );
  }

  /// 新しいFCMトークンが生成されたときに発生します。
  Stream<String> onTokenRefresh {を取得します。
    return _delegate.onTokenRefresh;
  }

  Future<bool> isSupported() { { デリゲート.isSupported()
    return _delegate.isSupported()}

  /// 現在の[NotificationSettings]を返す。
  ///
  /// パーミッションを要求するには、[requestPermission]を呼び出します。
  Future<NotificationSettings> getNotificationSettings() { /// 現在の[NotificationSettings]を返します。
    return _delegate.getNotificationSettings()}

  /// ユーザに通知の許可を求めます。
  ///
  /// - iOSでは、ユーザーの許可を求めるダイアログが表示されます。
  /// - macOSでは、許可を求める通知が表示されます。
  /// - Androidでは、[NotificationSettings]クラスが[NotificationSettings]の値で返されます。
  /// - Androidでは、[NotificationSettings.authorizationStatus]の値が返されます。
  アプリがシステム設定で通知を有効にしているか、ブロックしているかを示す /// [NotificationSettings.authorizationStatus]の値が返されます。
  /// - Web上では、ネイティブブラウザAPIを使用して、ユーザーの許可を要求するポップアップが表示されます。
  /

/// FCMサーバーに新しい[RemoteMessage]を送信します。Androidのみ。
  /// Firebaseは2024年6月に廃止されます: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessaging#send
  非推奨(
      'これは将来のリリースで削除されます。Firebaseは2024年6月に廃止予定')
  Future<void> sendMessage({
    文字列?
    Map<String, String>? data、
    文字列?
    文字列?
    string? messageType、
    int?
  }) {
    
      assert(ttl >= 0)}
    return _delegate.sendMessage(
      to: to ?'${app.options.messagingSenderId}@fcm.googleapis.com'、
      data: data、
      collapseKey: collapseKey、
      messageId: messageId、
      messageType: messageType、
      ttl: ttl、
    );
  }

  /// Firebase Cloud Messaging の自動初期化を有効または無効にします。
  Future<void> setAutoInitEnabled(bool enabled) async { /// Firebase Cloud Messagingの自動初期化を有効または無効にします。
    return _delegate.setAutoInitEnabled(enabled)}

  /// Android向けBigQueryへのFirebase Cloud Messagingメッセージ配信メトリクスのエクスポートを有効または無効にします。
  ///
  /// iOSでは、[このガイド](https://firebase.google.com/docs/cloud-messaging/understand-delivery?platform=ios#enable_delivery_data_export_for_background_notifications) に従う必要があります。
  /// BigQueryへのメトリクスエクスポートを行うには、[このガイド]() に従う必要があります。
  /// Webの場合は、[サービスワーカー](https://firebase.google.com/docs/cloud-messaging/js/client)をセットアップし、`experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messag)'を呼び出す必要があります。
JboyHashimotoJboyHashimoto

フォアグランドとバックグランド

どんなものか???

State(状態) Description(説明)
Foreground アプリケーションが開いているとき、表示されているとき、使用されているとき。
Background アプリケーションが開いているが、バックグラウンド(最小化)されている場合。これは通常、ユーザーがデバイスの「ホーム」ボタンを押したり、アプリ・スイッチャーで別のアプリに切り替えたり、別のタブ(ウェブ)でアプリケーションを開いたりした場合に発生します。
Terminated デバイスがロックされているか、アプリケーションが実行されていない場合。ユーザーは、端末のアプリ・スイッチャーUIから「スワイプして離す」、またはタブ(ウェブ)を閉じることで、アプリを終了させることができる。

アプリケーションが FCM 経由でメッセージペイロードを受信するには、いくつかの前提条 件を満たす必要があります:

FCMへの登録を可能にするために)アプリケーションが少なくとも1回開いていること。
iOSの場合、ユーザーがアプリスイッチャーからアプリをスワイプした場合、バックグラウンドメッセージが再び機能するように、手動でアプリを再度開く必要があります。
Androidの場合、ユーザーがデバイスの設定からアプリを強制終了すると、メッセージが動作するようになるため、再度手動でアプリを開く必要があります。
iOSおよびmacOSでは、FCMおよびAPNと統合するようにプロジェクトを正しくセットアップする必要があります。
ウェブ上では、「ウェブプッシュ証明書」のキーを持つトークンを(getToken経由で)要求している必要があります。

JboyHashimotoJboyHashimoto

Requesting permission (Apple & Web)

iOS、macOS、およびWebでは、FCMペイロードをデバイスで受信する前に、まずユーザーの許可を求める必要があります。Androidアプリケーションは許可を要求する必要はありません。

firebase_messagingパッケージは、requestPermissionメソッドを介してパーミッションを要求するためのシンプルなAPIを提供します。このAPIは、通知ペイロードを含むメッセージングがサウンドをトリガーできるかどうかや、Siri経由でメッセージを読み上げることができるかどうかなど、要求したいパーミッションのタイプを定義する多くの名前付き引数を受け付けます。デフォルトでは、このメソッドは、賢明なデフォルトのパーミッションを要求します。リファレンスAPIは、各パーミッションが何のためにあるのかについての完全なドキュメントを提供します。

開始するには、アプリケーションからメソッドを呼び出します(iOSではネイティブモーダルが表示され、WebではブラウザのネイティブAPIフローがトリガーされます):

このコードはAndroidでは不要???

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

The NotificationSettings class returned from the request details information regarding the user's decision.

The authorizationStatus property can return a value which can be used to determine the user's overall decision:

authorized: The user granted permission.
denied: The user denied permission.
notDetermined: The user has not yet chosen whether to grant permission.
provisional: The user granted provisional permission (see Provisional Permission).

リクエストから返されるNotificationSettingsクラスは、ユーザの決定に関する情報を詳述する。

authorizationStatus プロパティは、ユーザの全体的な決定を決定するために使用できる値を返すことができます:

許可された: ユーザが許可を与えた。
denied: ユーザーが許可を拒否した。
notDetermined: notDetermined:ユーザーがまだ許可を与えるかどうかを選択していない。
provisional: 暫定: ユーザーが暫定的な許可を与えた (暫定許可を参照)。

JboyHashimotoJboyHashimoto

StatefulWidgetでやるときの方法。

fcm tokenをlogに表示して、Firebaseのコンソールから直接送る方法の場合

logを出すコード:

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:hooks_example/firebase_options.dart';
import 'package:hooks_example/usestream.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // fcm tokenを取得
  final fcmToken = await FirebaseMessaging.instance.getToken();
  // fcm tokenを表示
  debugPrint('🐈fcm token: $fcmToken');
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const UserStreamExample(),
    );
  }
}

アプリ側でFCMから送信されたメッセージを受け取る方法:

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

class FcmExample extends StatefulWidget {
  const FcmExample({super.key});

  
  // ignore: library_private_types_in_public_api
  _FcmExampleState createState() => _FcmExampleState();
}

class _FcmExampleState extends State<FcmExample> {
  
  void initState() {
    super.initState();
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      debugPrint('🦁Got a message whilst in the foreground!');
      debugPrint('🐦Message data: ${message.data}');

      if (message.notification != null) {
        debugPrint('Message also contained a notification: ${message.notification}');
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM Example'),
      ),
      body: Center(
        child: Text('Waiting for messages...'),
      ),
    );
  }
}


JboyHashimotoJboyHashimoto

通知を受信するコード

画面を更新する必要があるので、Notifierを使う。

import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final messageNotifierProvider =
    StateNotifierProvider<MessageNotifier, AsyncValue<RemoteMessage>>((ref) {
  return MessageNotifier();
});

class MessageNotifier extends StateNotifier<AsyncValue<RemoteMessage>> {
  MessageNotifier() : super(const AsyncValue.loading()) {
    _messageSubscription = FirebaseMessaging.onMessage.listen((message) {
      state = AsyncValue.data(message);
    }, onError: (error, stackTrace) {
      state = AsyncValue.error(error, stackTrace);
    });
  }

  late final StreamSubscription<RemoteMessage> _messageSubscription;

  
  void dispose() {
    _messageSubscription.cancel();
    super.dispose();
  }
}

class FcmExample extends HookConsumerWidget {
  const FcmExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(messageNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM Example Riverpod!!'),
      ),
      body: Center(
        child: asyncValue.when(
          data: (message) {
            debugPrint('🦁Got a message whilst in the foreground!');
            debugPrint('🐦Message data: ${message.data}');

            if (message.notification != null) {
              debugPrint(
                  'Message also contained a notification: ${message.notification}');
            }

            return Column(
              children: [
                Text('Message data: ${message.data.toString()}'),
                if (message.notification != null)
                  Text('Notification title: ${message.notification!.title}'),
              ],
            );
          },
          loading: () => const Text('Waiting for messages...'),
          error: (_, __) => const Text('An error occurred!'),
        ),
      ),
    );
  }
}
JboyHashimotoJboyHashimoto

titleとbodyの情報を取得したい

Firebaseのコンソールから、通知のタイトルがtitleで、通知のテキストがbodyになる。

import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final messageNotifierProvider =
    StateNotifierProvider<MessageNotifier, AsyncValue<List<RemoteMessage>>>(
        (ref) {
  return MessageNotifier();
});

class MessageNotifier extends StateNotifier<AsyncValue<List<RemoteMessage>>> {
  MessageNotifier() : super(const AsyncValue.data(<RemoteMessage>[])) {
    _messageSubscription = FirebaseMessaging.onMessage.listen((message) {
      state.whenData((currentMessages) {
        state = AsyncValue.data([...currentMessages, message]);
      });
    }, onError: (error, stackTrace) {
      state = AsyncValue.error(error, stackTrace);
    });
  }

  late final StreamSubscription<RemoteMessage> _messageSubscription;

  
  void dispose() {
    _messageSubscription.cancel();
    super.dispose();
  }
}

class FcmExample extends HookConsumerWidget {
  const FcmExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(messageNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM Example Riverpod'),
      ),
      body: Center(
        child: asyncValue.when(
          data: (messages) {
            return ListView.builder(
              itemCount: messages.length,
              itemBuilder: (context, index) {
                final message = messages[index];
                return ListTile(
                  title: Text('Message data: ${message.notification!.title}'),
                  subtitle: message.notification != null
                      ? Text(
                          'Notification title: ${message.notification!.body}')
                      : null,
                );
              },
            );
          },
          loading: () => const CircularProgressIndicator(),
          error: (_, __) => const Text('An error occurred!'),
        ),
      ),
    );
  }
}
JboyHashimotoJboyHashimoto

riverpod2系だとどうするのか???

StreamNotifierを使えば画面の更新ができて、ずっと監視する機能を作れた???

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'fcm_stream_notifier.g.dart';


class FcmStreamNotifier extends _$FcmStreamNotifier {

  // Stream<List<RemoteMessage>>のデータ型を返す
  
  Stream<List<RemoteMessage>> build() {
    // listという空のリストを作成
    var list = <RemoteMessage>[];
    // FirebaseMessaging.onMessage.mapでメッセージを受け取る
    return FirebaseMessaging.onMessage.map((message) {
      // listにFirebaseMessaging.onMessage.mapで受け取ったメッセージを追加
      list.add(message);
      // onDisposeでlistをクリア
       ref.onDispose(() {
        list.clear();
       });
      return list;
    });
  }
}

View側のコード

import 'package:flutter/material.dart';
import 'package:hooks_example/fcm_example/fcm_stream_notifier.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class FcmStreamView extends HookConsumerWidget {
  const FcmStreamView({super.key});
  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(fcmStreamNotifierProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('FCM Stream Riverpod🐈'),
      ),
      body: Center(
        child: asyncValue.when(
          data: (messages) {
            return ListView.builder(
              itemCount: messages.length,// mstの数だけリストを作成
              itemBuilder: (context, index) {
                final message = messages[index];
                return ListTile(
                  title: Text('Message data: ${message.notification!.title}'),// タイトルを表示
                  subtitle: message.notification != null
                      ? Text(
                          'Notification title: ${message.notification!.body}')// テキストの内容
                      : null,
                );
              },
            );
          },
          loading: () => const CircularProgressIndicator(),
          error: (error, stackTrace) => Text('Error: $error'),
        ),
      ),
    );
  }
}