navigatorState在使用GetMaterialPage的pushNamed Navigation onGene



我使用flutter_callkit_incoming包在我的应用程序中通过FCM的有效载荷在我的应用程序的所有状态(即后台/前台/终止状态)获取callsNotification。

导航是很好的现在到videocallingagorpage后,点击接受按钮呼叫传入通知在我的应用程序的前景状态。->使用NikahMatch类中的listenerEvent

但是当这个listenerEvent被用于后台/终止状态的导航时,问题就来了。 →使用listenerEvent作为顶级函数由于我的背景的后台处理程序如下所示处理函数

当编译器在我的应用程序的后台/终止状态下单击flutter_callKit_incoming的通知接受时,在侦听器事件中读取await NavigationService.instance.pushNamed(AppRoute.voiceCall);这行,我在控制台上得到这个错误。

E/flutter (11545): Receiver: null
E/flutter (11545): Tried calling: pushNamed<Object>("/videoCall_agora", arguments: null)
E/flutter (11545): #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:38:5)
E/flutter (11545): #1      NavigationService.pushNamed (package:nikah_match/helpers/navigationService.dart:38:39)
E/flutter (11545): #2      listenerEvent.<anonymous closure> (package:nikah_match/main.dart:311:46)
E/flutter (11545): <asynchronous suspension>
E/flutter (11545): 

以及在日志中,我发现在pushNamed函数中定义的log(navigationKey.currentState.toString());也是null。而在前景导航中,navigationKey。现状后从pushNamed函数是永远不会空。

当我收到终止状态的调用通知时,在没有创建小部件树和初始化GetMaterialPage导致导航器状态为null的情况下调用侦听器事件(顶级函数)。

我认为listnerEvent Accept案例是在启动/构建小部件树之前运行的,并且GetMaterialPage中的导航键从未分配过。

我怎么去掉?

这是我的backgroundHandler函数:
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
bool videoCallEnabled = false;
bool audioCallEnabled = false;
if (message != null) {
debugPrint("Handling background is called");
print(
"Handling a background message and background handler: ${message.messageId}");
try {
videoCallEnabled = message.data.containsKey('videoCall');
audioCallEnabled = message.data.containsKey('voiceCall');
if (videoCallEnabled || audioCallEnabled) {
log("Video call is configured and is started");
showCallkitIncoming(Uuid().v4(), message: message);
//w8 for streaming
debugPrint("Should listen to events in background/terminated state");
listenerEvent(message);
} else {
log("No Video or audio call was initialized");
}
} catch (e) {
debugPrint("Error occured:" + e.toString());
}
}
}

这是我的监听事件:

Future<void> listenerEvent(RemoteMessage message) async {

log("Listner event background/terminated handler app has run");
backgroundChatRoomId = message.data['chatRoomId'];
backgroundCallsDocId = message.data['callsDocId'];
backgroundRequesterName = message.data['callerName'];
backgroundRequesterImageUrl = message.data['imageUrl'];
// String imageUrl = message.data['imageUrl'];
bool videoCallEnabled = false;

if (message.data != null) {
videoCallEnabled = message.data.containsKey('videoCall');
} else {
log("Data was null");
}
try {
FlutterCallkitIncoming.onEvent.listen((event) async {
print('HOME: $event');
switch (event.name) {
case CallEvent.ACTION_CALL_INCOMING:
// TODO: received an incoming call
log("Call is incoming");
break;
case CallEvent.ACTION_CALL_START:
// TODO: started an outgoing call
// TODO: show screen calling in Flutter
log("Call is started");
break;
case CallEvent.ACTION_CALL_ACCEPT:
// TODO: accepted an incoming call
// TODO: show screen calling in Flutter
log("......Call Accepted background/terminated state....");
currentChannel = backgroundChatRoomId;
log("currentChannel in accepted is: $currentChannel");
debugPrint("Details of call"+backgroundChatRoomId+backgroundCallsDocId );
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(backgroundChatRoomId)
.collection("calls")
.doc(backgroundCallsDocId)
.update({
'receiverCallResponse': 'Accepted',
'callResponseDateTime': FieldValue.serverTimestamp()
}).then((value) => log("Values updated at firebase firestore as Accepted"));
if (videoCallEnabled) {
log("in video call enabled in accept call of listener event");
await NavigationService.instance.pushNamed(AppRoute.videoAgoraCall,);

} 
break;
}
});
} on Exception {}
}

这是我的第一个有状态GetMaterial页面,它初始化了所有Firebase消息传递功能(Forground Local FLutter本地通知从代码中排除,以提高可读性):

class NikkahMatch extends StatefulWidget {
const NikkahMatch({Key key}) : super(key: key);
@override
State<NikkahMatch> createState() => _NikkahMatchState();
}
class _NikkahMatchState extends State<NikkahMatch> with WidgetsBindingObserver {



@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addObserver(this);
//Function if from terminated state
FirebaseMessaging.instance.getInitialMessage().then((message) async {
log("get Initial Message function is used.. ");
String screenName = 'No screen';
bool screenEnabled = false;
if (message != null) {
if (message.data != null) {
log("Remote message data is null for now");
if (message.data.isNotEmpty) {
screenEnabled = message.data.containsKey('screenName');
if (screenEnabled) {
if (screenName == 'chatScreen') {
log("Screen is Chat");
String type = 'Nothing';
String chatRoomId = 'Nothing';
if (message.data['type'] != null) {
type = message.data['type'];
if (type == 'profileMatched') {
String likerId = message.data['likerId'];
String likedId = message.data['likedId'];
chatRoomId = chatController.getChatRoomId(likerId, likedId);
}
} else {
chatRoomId = message.data['chatRoomId'];
}
log("ChatRoom Id is: ${chatRoomId}");
log("Navigating from onMessagePop to the ChatRoom 1");
//We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(chatRoomId)
.get()
.then((value) async {
if (value.exists) {
log("ChatRoom Doc " + value.toString());
log("Navigating from onMessagePop to the ChatRoom 2");
log("Last Message was : ${value.data()['lastMessage']}");
backGroundLevelChatRoomDoc = value.data();

await NavigationService.instance.pushNamed(AppRoute.chatScreen);
} else {
log("no doc exist for chat");
}
});
}
else if (screenName == 'videoScreen') {
log("Screen is Video");
initCall(message);
} else if (screenName == 'voiceScreen') {
log("Screen is Audio");
initCall(message);
} else {
log("Screen is in Else method of getInitialMessage");
}

} else {
debugPrint("Notification Pay load data is Empty");
}
} else {
log("Screen isn't enabled");
}
} else {
log("message data is null");
}
} else {
log("...........message data is null in bahir wala else");
}
});

//This function will constantly listen to the notification recieved from firebase
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
log("onMessageOpenedApp function is used.. ");
String screenName = 'No screen';
bool screenEnabled = false;
if (message.data.isNotEmpty) {
screenEnabled = message.data.containsKey('screenName');
if (screenEnabled) {
//Move to the screen which is needed to
log("Screen is Enabled");
screenName = message.data['screenName'];
log("Screen name is: $screenName");
if (screenName == 'chatScreen') {
log("Screen is Chat");
String type = 'Nothing';
String chatRoomId = 'Nothing';
if (message.data['type'] != null) {
type = message.data['type'];
if (type == 'profileMatched') {
String likerId = message.data['likerId'];
String likedId = message.data['likedId'];
chatRoomId = chatController.getChatRoomId(likerId, likedId);
}
} else {
chatRoomId = message.data['chatRoomId'];
}
log("ChatRoom Id is: ${chatRoomId}");
log("Navigating from onMessagePop to the ChatRoom 1");
//We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(chatRoomId)
.get()
.then((value) async {
if (value.exists) {
log("ChatRoom Doc " + value.toString());
log("Navigating from onMessagePop to the ChatRoom 2");
log("Last Message was : ${value.data()['lastMessage']}");
backGroundLevelChatRoomDoc = value.data();
/*     await NavigationService.instance
.pushNamed(AppRoute.chatScreen, args:ChatArgs(value.data(), false));*/
await NavigationService.instance.pushNamed(AppRoute.chatScreen);
} else {
log("no doc exist for chat");
}
});
}
else if (screenName == 'videoScreen') {
log("Screen is Video");
initCall(message);
} else if (screenName == 'voiceScreen') {
log("Screen is Audio");
initCall(message);
} else {
log("Screen is in Else");
}
}
} else {
debugPrint("Notification Pay load data is Empty");
}
});
}
@override
Widget build(BuildContext context) {
print("Main page build");
return GetMaterialApp(
onGenerateRoute: AppRoute.generateRoute,
debugShowCheckedModeBanner: false,
navigatorKey: NavigationService.instance.navigationKey,
debugShowMaterialGrid: false,
title: 'Nikah Match',
initialRoute: '/splash_screen',
theme: ThemeData(
fontFamily: 'Poppins',
scaffoldBackgroundColor: kScaffoldBgColor,
appBarTheme: const AppBarTheme(
elevation: 0,
backgroundColor: kPrimaryColor,
),
accentColor: kPrimaryColor.withOpacity(0.2),
),
themeMode: ThemeMode.light,
getPages: [
GetPage(name: '/splash_screen', page: () => SplashScreen()),
GetPage(name: '/get_started', page: () => GetStarted()),
GetPage(
name: '/videoCall_agora',
page: () => VideoCallAgoraUIKit(
anotherUserName: backgroundRequesterName,
anotherUserImage: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: "",
anotherUserId: "",
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
),
),
// GetPage(name: '/after_log_in_screen', page: () => AfterLogin()),
],
);
}
}
这是我的NavigationService类:
class NavigationService {
// Global navigation key for whole application
GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>();
/// Get app context
BuildContext get appContext => navigationKey.currentContext;
/// App route observer
RouteObserver<Route<dynamic>> routeObserver = RouteObserver<Route<dynamic>>();
static final NavigationService _instance = NavigationService._private();
factory NavigationService() {
return _instance;
}
NavigationService._private();
static NavigationService get instance => _instance;
/// Pushing new page into navigation stack
///
/// `routeName` is page's route name defined in [AppRoute]
/// `args` is optional data to be sent to new page
Future<T> pushNamed<T extends Object>(String routeName,
{Object args}) async {
log(navigationKey.toString());
log(navigationKey.currentState.toString());
return navigationKey.currentState.pushNamed<T>(
routeName,
arguments: args,
);
}
Future<T> pushNamedIfNotCurrent<T extends Object>(String routeName,
{Object args}) async {
if (!isCurrent(routeName)) {
return pushNamed(routeName, args: args);
}
return null;
}
bool isCurrent(String routeName) {
bool isCurrent = false;
navigationKey.currentState.popUntil((route) {
if (route.settings.name == routeName) {
isCurrent = true;
}
return true;
});
return isCurrent;
}
/// Pushing new page into navigation stack
///
/// `route` is route generator
Future<T> push<T extends Object>(Route<T> route) async {
return navigationKey.currentState.push<T>(route);
}
/// Replace the current route of the navigator by pushing the given route and
/// then disposing the previous route once the new route has finished
/// animating in.
Future<T> pushReplacementNamed<T extends Object, TO extends Object>(
String routeName,
{Object args}) async {
return navigationKey.currentState.pushReplacementNamed<T, TO>(
routeName,
arguments: args,
);
}
/// Push the route with the given name onto the navigator, and then remove all
/// the previous routes until the `predicate` returns true.
Future<T> pushNamedAndRemoveUntil<T extends Object>(
String routeName, {
Object args,
bool Function(Route<dynamic>) predicate,
}) async {
return navigationKey.currentState.pushNamedAndRemoveUntil<T>(
routeName,
predicate==null?  (_) => false: (_) => true,
arguments: args,
);
}
/// Push the given route onto the navigator, and then remove all the previous
/// routes until the `predicate` returns true.
Future<T> pushAndRemoveUntil<T extends Object>(
Route<T> route, {
bool Function(Route<dynamic>) predicate,
}) async {
return navigationKey.currentState.pushAndRemoveUntil<T>(
route,
predicate==null?  (_) => false: (_) => true,
);
}
/// Consults the current route's [Route.willPop] method, and acts accordingly,
/// potentially popping the route as a result; returns whether the pop request
/// should be considered handled.
Future<bool> maybePop<T extends Object>([Object args]) async {
return navigationKey.currentState.maybePop<T>(args as T);
}
/// Whether the navigator can be popped.
bool canPop() => navigationKey.currentState.canPop();
/// Pop the top-most route off the navigator.
void goBack<T extends Object>({T result}) {
navigationKey.currentState.pop<T>(result);
}
/// Calls [pop] repeatedly until the predicate returns true.
void popUntil(String route) {
navigationKey.currentState.popUntil(ModalRoute.withName(route));
}
}
class AppRoute {
static const homePage = '/home_page';
static const chatScreen ='/chat_screen';
static const splash = '/splash_screen';
static const voiceCall = '/voice_call';
static const videoAgoraCall = '/videoCall_agora';
static Route<Object> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homePage:
return MaterialPageRoute(
builder: (_) => HomePage(), settings: settings);
case chatScreen:
return MaterialPageRoute(
builder: (_) =>
ChatScreen(docs: backGroundLevelChatRoomDoc, isArchived: false,), settings: settings);
case splash:
return MaterialPageRoute(
builder: (_) =>  SplashScreen(), settings: settings);
case voiceCall:
return MaterialPageRoute(
builder: (_) =>  VoiceCall(
toCallName: backgroundRequesterName,
toCallImageUrl: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: voiceCallToken,
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
), settings: settings);
case videoAgoraCall:
return MaterialPageRoute(
builder: (_) =>  VideoCallAgoraUIKit(
anotherUserName: backgroundRequesterName,
anotherUserImage: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: "",
anotherUserId: "",
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
), settings: settings);
default:
return null;
}
}
}

实际上,当我在终止/背景状态下使用flutter_incoming_callkit进行导航时,我也被卡住了几周,令人惊讶的是,解决方案非常简单。

导致这个问题的原因是:

→对于在终止或后台状态下接收通知,backgroundHandler函数在其自己的隔离中工作。而NikkahMatch类中的onGenerateRoutes对于这个孤立的函数来说是不知道的,在这个函数中,你实际上是在尝试导航pushNamed路由。

所以我的解决方案是:

→结合firestore和cloud功能进行导航。这将允许我们有上下文,它不会是空的,因为我们是从应用程序小部件树和中导航,而不是从一个孤立的函数,如backgroundHandler。顶层listenerEvent函数只用于在Firestore上更改调用文档中的值。

[注:顶层函数是不属于任何类的函数]

接收端收到flutter_incoming_callkit通知时:

在单击Accept按钮时,使用顶级listenerEvent函数将呼叫状态从传入更改为呼叫文档中的已接受。这将从终止/后台状态打开应用程序。

我在我的第一类小部件树中使用了这个函数didChangeAppLifecycleState来处理/知道应用程序是否来自终止/后台状态:看看下面的代码:

Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
print(state);
if (state == AppLifecycleState.resumed) {
//Check call when open app from background
print("in app life cycle resumed");
checkAndNavigationCallingPage();
}
}
checkAndNavigationCallingPage() async {
print("checkAndNavigationCallingPage CheckCalling page 1");
if (auth.currentUser != null) {
print("auth.currentUser.uid: ${auth.currentUser.uid}");
}
// NavigationService().navigationKey.currentState.pushNamed(AppRoute.videoAgoraCall);
var currentCall = await getCurrentCall();
print("inside the checkAndNavigationCallingPage and $currentCall");
if (currentCall != null) {
print("inside the currentCall != null");
//Here we have to move to calling page
final g = Get;
if (!g.isDialogOpen) {
g.defaultDialog(content: CircularProgressIndicator());
}
Future.delayed(Duration(milliseconds: 50));
await FirebaseFirestore.instance
.collection('Users')
.doc(auth.currentUser.uid)
.collection('calls')
.doc(auth.currentUser.uid)
.get()
.then((userCallDoc) async {
if (userCallDoc.exists) {
if (userCallDoc['type'] == 'chapVoiceCall' ||
userCallDoc['type'] == 'chapVideoCall') {
bool isDeclined = false;
String chapCallDocId = "";
chapCallDocId = userCallDoc['chapCallDocId'];
print(
"............. Call was Accepted by receiver or sender.............");
print("ChapCallDocId $chapCallDocId");
ChapCallDocModel cdm = ChapCallDocModel();
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(userCallDoc['chatRoomId'])
.collection("chapCalls")
.doc(chapCallDocId)
.get()
.then((value) {
if ((value['requestedResponse'] == 'Declined' &&
value['requesterResponse'] == 'Declined') ||
(value['senderResponse'] == 'Declined') ||
(value['requestedResponse'] == 'TimeOut' &&
value['requesterResponse'] == 'TimeOut')) {
isDeclined = true;
print(
"in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
} else {
isDeclined = false;
cdm = ChapCallDocModel.fromJson(value.data());
print("CDM print is: ${cdm.toJson()}");
}
});
currentChannel = userCallDoc['chatRoomId'];
if (!isDeclined) {
if (userCallDoc['type'] == 'chapVoiceCall') {
print("in voice call enabled in accept call of listener event");
var voiceCallToken = await GetToken().getTokenMethod(
userCallDoc['chatRoomId'], auth.currentUser.uid);
print("token before if in splashscreen is: ${voiceCallToken}");
if (voiceCallToken != null) {
if (g.isDialogOpen) {
g.back();
}
Get.to(
() => ChapVoiceCall(
toCallName: userCallDoc['requesterName'],
toCallImageUrl: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: voiceCallToken,
docId: userCallDoc['chapCallDocId'],
callDoc: cdm,
),
);
} else {
print(
"......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
}
} else {
if (g.isDialogOpen) {
g.back();
}
g.to(() => ChapVideoCallAgoraUIKit(
anotherUserName: userCallDoc['requesterName'],
anotherUserImage: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: "",
anotherUserId: userCallDoc['requesterId'],
docId: userCallDoc['chapCallDocId'],
callDoc: cdm,
));
}
} else {
await FlutterCallkitIncoming.endAllCalls();
print(
"the call was either declined by sender or receiver or was timed out.");
}
} else {
bool isDeclined = false;
print(
"............. Call was Accepted by receiver or sender.............");
CallDocModel cdm = CallDocModel();
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(userCallDoc['chatRoomId'])
.collection("calls")
.doc(userCallDoc['callsDocId'])
.get()
.then((value) {
if (value['receiverCallResponse'] == 'Declined' ||
value['senderCallResponse'] == 'Declined' ||
value['receiverCallResponse'] == 'TimeOut' ||
value['senderCallResponse'] == 'TimeOut') {
isDeclined = true;
print(
"in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
} else {
isDeclined = false;
cdm = CallDocModel.fromJson(value.data());
print("CDM print is: ${cdm.toJson()}");
}
});
currentChannel = userCallDoc['chatRoomId'];
if (!isDeclined) {
if (userCallDoc['type'] == 'voiceCall') {
print("in voice call enabled in accept call of listener event");
var voiceCallToken = await GetToken().getTokenMethod(
userCallDoc['chatRoomId'], auth.currentUser.uid);
print("token before if in splashscreen is: ${voiceCallToken}");
if (voiceCallToken != null) {
if (g.isDialogOpen) {
g.back();
}
Get.to(
() => VoiceCall(
toCallName: userCallDoc['requesterName'],
toCallImageUrl: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: voiceCallToken,
docId: userCallDoc['callsDocId'],
callDoc: cdm,
),
);
} else {
print(
"......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
}
} else {
if (g.isDialogOpen) {
g.back();
}
g.to(() => VideoCallAgoraUIKit(
anotherUserName: userCallDoc['requesterName'],
anotherUserImage: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: "",
anotherUserId: userCallDoc['requesterId'],
docId: userCallDoc['callsDocId'],
callDoc: cdm,
));
}
} else {
await FlutterCallkitIncoming.endAllCalls();
print(
"the call was either declined by sender or receiver or was timed out.");
}
}
} else {
debugPrint("Document not found");
}
});
}
}
在上面的代码中,我给出了我的db场景,所以任何需要知道如何处理不同调用状态的人都可以深入研究它。或者你可以在这里评论,我很荣幸回复。

最新更新