I'm using flutter_callkit_incoming package to get callsNotification in my application through the payload of FCM in all states of my App i.e background/forground/terminated state.
Navigation is fine now to the VideoCallingAgoraPage after clicking Accept button on call incoming notification on forground state of my app. -> Using listenerEvent from NikahMatch class
But problem comes when this listenerEvent is used for navigation in background/terminated state. -> Using listenerEvent as top level function because of background handler as shown below in my background handler function
When the compiler reads this line await NavigationService.instance.pushNamed(AppRoute.voiceCall);
in listener event on clicking accept of notification from flutter_callKit_incoming in the background/terminated state of my app, I am getting this error in console.
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):
As well as in the logs, I find that log(navigationKey.currentState.toString());
defined in pushNamed function is also null
. While in the case of forground navigation, navigationKey.currentState from pushNamed function is never null.
When I received call notification in terminated state, accept case of listener event(top level function) was called without creating widget tree and initializing GetMaterialPage that caused navigator state to be null.
I think that the listnerEvent Accept case is run before starting/building widget tree and navigator key in GetMaterialPage is never assigned.
How can I get rid of that?
This is my backgroundHandler function:
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());
}
}
}
This is my listener event:
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 {}
}
This is my first stateful GetMaterial page which initializes all Firebase Messaging functions (Forground Local FLutter local notifications excluded from code for readability):
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()),
],
);
}
}
This is my NavigationService class:
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;
}
}
}