You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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等多种路由方式,均未达到预期效果。

我的问题:

  1. 如何在onBackgroundMessage中调用MethodChannel,执行MainActivity.kt中的函数?
  2. 收到消息后如何实现路由跳转?

相关代码

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中直接执行屏幕解锁逻辑:

  1. 创建自定义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)
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:27:03