Flutter中FCM onBackgroundMessage调用MethodChannel及路由问题求助
我需要在手机锁屏上显示提醒界面,目前使用flutter_local_notifications尝试通过全屏意图启动界面并跳转路由,但在FirebaseMessaging的onBackgroundMessage中调用MethodChannel时,出现错误:
MissingPluginException(No implementation found for method turnScreenOnAndKeyguardOff on channel com.example.myapp/actions)
同时我尝试了FirebaseMessaging的onInitialMessage、onMessage、onBackgroundMessage,以及flutter_local_notifications的onDidLaunchApp等多种路由方式,均未达到预期效果。
我的问题:
- 如何在
onBackgroundMessage中调用MethodChannel,执行MainActivity.kt中的函数? - 收到消息后如何实现路由跳转?
main.dart
FlutterLocalNotificationsPlugin notificationsPlugin = FlutterLocalNotificationsPlugin(); void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); await FirebaseAuthImpl().signInAnonymously(); FirebaseAlert().initNotifications(); runApp( const App(), ); }
firebase_alert.dart
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:alarmplayer/alarmplayer.dart'; import 'package:flutter/services.dart'; import 'package:myapp/services/notification_service.dart'; Future<void> handleBackgroundMessage(RemoteMessage message) async { await Firebase.initializeApp(); print('Title : ${message.notification?.title}'); print('Body : ${message.notification?.body}'); print('Payload : ${message.data}'); Future<void> playAudio(String soundName) async { Alarmplayer alarmplayer = Alarmplayer(); bool playing = false; void switchPlaying() { playing = !playing; } try { await alarmplayer.Alarm( url: "assets/sounds/$soundName.wav", volume: 1, looping: false, callback: switchPlaying, ); } catch (e) { print('Error playing sound: $e'); } } if (message.data.containsKey('alert_type_name')) { final String alertTypeName = message.data['alert_type_name']; print('Alert Type: $alertTypeName'); } if (message.data.containsKey("alert_type_name")) { final String alertTypeName = message.data["alert_type_name"]; // await playAudio(alertTypeName); } await futureShowNotification(); } class FirebaseAlert { Future<void> initNotifications() async { FirebaseMessaging firebaseMessaging = FirebaseMessaging.instance; await firebaseMessaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: true, criticalAlert: true, provisional: false, sound: true, ); FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage); final fCMToken = await firebaseMessaging.getToken(); print('token: $fCMToken'); } }
notification_service.dart
import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:myapp/main.dart'; Future<void> initNotifications() async { AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings("darohicon"); var initializationSettingsIOS = const DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestCriticalPermission: true, requestProvisionalPermission: true, requestSoundPermission: true ); var initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS ); await notificationsPlugin.initialize(initializationSettings,onDidReceiveBackgroundNotificationResponse:(NotificationResponse notificationResponse) async { }); } notificationDetails(){ return const NotificationDetails( android: AndroidNotificationDetails("high_importance_channel", "High Importance Notifications",icon:"@mipmap/darohicon",importance: Importance.max,fullScreenIntent: true), iOS: DarwinNotificationDetails() ); } Future<void> futureShowNotification({ int id = 0, String? title, String? body, String? payload }) async { MethodChannel platform = await const MethodChannel('com.example.myapp/actions'); await _turnScreenOnAndKeyguardOff(platform); // The screen-related action is complete at this point // Perform other tasks notificationsPlugin.cancelAll(); return notificationsPlugin.show(id, title, body, await notificationDetails()); } Future<void> _turnScreenOnAndKeyguardOff(MethodChannel platform) async { try { await platform.invokeMethod('turnScreenOnAndKeyguardOff'); } on PlatformException catch (e) { print("Error: ${e.message}"); } } Future<void> _restoreDefaultFlags(MethodChannel platform) async { try { await platform.invokeMethod('restoreDefaultFlags'); } on PlatformException catch (e) { print("Error: ${e.message}"); } }
mainActivity.kt
package com.example.myapp import android.content.Context import android.os.Build import android.os.Bundle import android.view.WindowManager import android.app.KeyguardManager import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingPlugin class MainActivity : io.flutter.embedding.android.FlutterActivity() { private val CHANNEL = "com.example.myapp/actions" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) flutterEngine?.plugins?.add(FlutterFirebaseMessagingPlugin()) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "turnScreenOnAndKeyguardOff" -> { turnScreenOnAndKeyguardOff() result.success(null) } "restoreDefaultFlags" -> { restoreDefaultFlags() result.success(null) } else -> result.notImplemented() } } } private fun turnScreenOnAndKeyguardOff() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) setTurnScreenOn(true) } else { window.addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON ) } with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { requestDismissKeyguard(this@MainActivity, null) } } } private fun restoreDefaultFlags() { // Remove the flags that were set in turnScreenOnAndKeyguardOff if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(false) setTurnScreenOn(false) } else { window.clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON ) } } }
问题1:在onBackgroundMessage中调用MethodChannel的解决方法
onBackgroundMessage运行在隔离线程中,此时Flutter引擎未完全初始化,无法直接和绑定前台FlutterEngine的MethodChannel通信。推荐以下两种方案:
方案A:原生层直接处理后台消息(优先选择)
跳过Flutter层,在Android原生的FirebaseMessagingService中直接执行屏幕解锁逻辑:
- 创建自定义FirebaseMessagingService:
package com.example.myapp import android.content.Context import android.os.Build import android.app.KeyguardManager import android.app.NotificationManager import android.content.Intent import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) // 直接执行屏幕解锁逻辑 turnScreenOnAndKeyguardOff() // 显示全屏通知 showFullScreenNotification(remoteMessage) } private fun turnScreenOnAndKeyguardOff() { // 唤醒并解锁屏幕 val intent = Intent(this, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) startActivity(intent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) setTurnScreenOn(true) } else { val window = (getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager) window.defaultDisplay?.apply { val attrs = android.view.WindowManager.LayoutParams() attrs.flags = android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or android.view.WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON } } val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { keyguardManager.requestDismissKeyguard(null, null) } } private fun showFullScreenNotification(remoteMessage: RemoteMessage) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val intent = Intent(this, MainActivity::class.java) intent.putExtra("payload", remoteMessage.data) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) val pendingIntent = android.app.PendingIntent.getActivity( this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE ) val notification = NotificationCompat.Builder(this, "high_importance_channel") .setContentTitle(remoteMessage.notification?.title) .setContentText(remoteMessage.notification?.body) .setSmallIcon(R.mipmap.darohicon) .setPriority(NotificationCompat.PRIORITY_MAX) .setFullScreenIntent(pendingIntent, true) .build() notificationManager.notify(1, notification) } }
- 在AndroidManifest.xml中注册该Service:
<service android:name=".MyFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>
方案B:通过IsolateNameServer传递通信端口(不推荐,可靠性低)
在MainActivity初始化时,将FlutterEngine的BinaryMessenger注册到IsolateNameServer:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) android.os.IsolateNameServer.registerPortWithName( flutterEngine.dartExecutor.binaryMessenger, "main_channel" ) // 保留原有的MethodChannel代码 }
然后在Flutter的onBackgroundMessage中获取该端口并创建MethodChannel:
Future<void> handleBackgroundMessage(RemoteMessage message) async { await Firebase.initializeApp(); final port = IsolateNameServer.lookupPortByName("main_channel"); if (port != null) { final channel = MethodChannel('com.example.myapp/actions', JSONMethodCodec(), port); try { await channel.invokeMethod('turnScreenOnAndKeyguardOff'); } catch (e) { print(e); } } // 其他逻辑 }
注意:该方式依赖前台Isolate存活,后台场景下可能失效。
问题2:收到消息后的路由跳转实现
路由跳转需要区分前台、后台、未启动三种状态:
1. 应用前台状态(onMessage)
使用全局NavigatorKey直接跳转:
// 全局定义NavigatorKey final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); // 在App中配置 class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: navigatorKey, routes: { '/': (context) => const HomeScreen(), '/alert-screen': (context) => AlertScreen(), }, ); } } // 监听前台消息 FirebaseMessaging.onMessage.listen((RemoteMessage message) { Navigator.of(navigatorKey.currentContext!).pushNamed('/alert-screen', arguments: message.data); });
2. 应用未启动状态(冷启动)
在main函数中获取初始消息,启动后跳转:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // 获取冷启动时的消息 RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { // 全局保存消息 GlobalData.initialMessage = initialMessage; } runApp(const App()); } // 在首页initState中处理跳转 class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override void initState() { super.initState(); if (GlobalData.initialMessage != null) { WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.of(context).pushNamed('/alert-screen', arguments: GlobalData.initialMessage!.data); GlobalData.initialMessage = null; }); } } @override Widget build(BuildContext context) { return Scaffold(); } }
3. 应用后台状态(唤醒)
通过flutter_local_notifications的回调处理:
await notificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse response) { if (response.payload != null) { Map<String, dynamic> data = jsonDecode(response.payload!); Navigator.of(navigatorKey.currentContext!).pushNamed('/alert-screen', arguments: data); } }, );
注意:后台点击通知会先唤醒应用到前台,此时需要通过通知payload传递数据,在前台回调中处理跳转。
- Android需添加权限到AndroidManifest.xml:
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
- 全屏通知必须设置
importance: Importance.max和fullScreenIntent: true,且通知渠道为高优先级。 onBackgroundMessage中无法直接操作UI或路由,必须通过原生或前台Isolate中转。
内容的提问来源于stack exchange,提问作者eiTey trainers

