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;
}
}
}
Actually, I was also stuck when using flutter_incoming_callkit for navigation in terminated/background state for weeks, and strikingly, the solution was so simple.
The reason causing you this problem is:
-> For receiving notification in terminated or background state, backgroundHandler function is working in its own isolate and onGenerateRoutes from your NikkahMatch class is not and never known to this isolated function where you are actually trying to navigate through the pushNamed route.
So my solution was:
-> Use the combination of firestore and cloud functions for navigation. This would allow us to have the context and it won't be null as we are navigating from inside the app widget tree and not from an isolated function i.e backgroundHandler. Top-level listenerEvent Function is only used to change the values in the call document on Firestore.
[Note: Top-Level function is a function that is not part of any class]
Upon receiving flutter_incoming_callkit notification on receiver side:
On the click of Accept button, use top-level listenerEvent function to change the call status from incoming to accepted in call's document.
This will open the app from terminated/background state.
I used this function didChangeAppLifecycleState in my first class of widget tree to handle/know if app has come from terminated/background state:
Check out this code:
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");
}
});
}
}
In the above code, I have given my db scenario, so anyone, who needs to know how to handle the different call status can deeply look into it. Or you can comment here and I would be honored to reply.
Related
I have a simple controller like this
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
user = user;
print(user.sId);
notifyListeners();
}
login(data) async {
var response = await ApiService().login(data);
final databody = json.decode(response);
if (databody['success']) {
UserData authUser = UserData.fromJson(databody['data']);
setUser(authUser);
notifyListeners();
return true;
} else {
return false;
}
}
}
I am trying to just print it like this on both widget and in initstate function but values are showing null. I can see in set function value is not null.
print('id ${context.watch<UserController>().user.sId.toString()}');
print(
'id2 ${Provider.of<UserController>(context, listen: false).user.sId.toString()}');
I already have added
ChangeNotifierProvider(create: (_) => UserController()),
],
in main.dart in MultiProvider
Also on Tap of login button I am doing this
showLoader(context);
UserController auth = Provider.of<UserController>(
context,
listen: false);
var data = {
"userEmail":
emailController.text.trim().toLowerCase(),
"userPassword": passwordController.text.trim(),
};
auth.login(data).then((v) {
if (v) {
hideLoader(context);
context.go('/homeroot');
} else {
hideLoader(context);
Fluttertoast.showToast(
backgroundColor: green,
textColor: Colors.white,
msg:
'Please enter correct email and password');
}
});
Try to include this while naming is same,
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Follow this structure
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Future<bool> login(String data) async {
await Future.delayed(Duration(seconds: 1));
UserData authUser = UserData(sId: data);
setUser(authUser);
notifyListeners();
return true;
}
}
class HPTest extends StatelessWidget {
const HPTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<UserController>(
builder: (context, value, child) {
return Text(value.user.sId);
},
),
floatingActionButton: FloatingActionButton(onPressed: () async {
final result = await Provider.of<UserController>(context, listen: false)
.login("new ID");
print("login $result");
;
}),
);
}
}
Here is the Class of Local Notification Service which will Call while the Notification Came
class LocalNotificationService {
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
static void initialize() {
// initializationSettings for Android
const InitializationSettings initializationSettings =
InitializationSettings(
android: AndroidInitializationSettings("#mipmap/ic_launcher"),
);
_notificationsPlugin.initialize(
initializationSettings,
onDidReceiveBackgroundNotificationResponse: (details) {
print("details Data : ${details.payload}");
if (details.payload == 'Offer Section' && details.payload != null) {
if (tokenValue != null) {
Navigator.push(
navigatorkey.currentContext!,
MaterialPageRoute(
builder: (context) => OfferMeScreen(),
));
}
} else if (details.payload == 'Surprise Me Section' &&
details.payload != null) {
Navigator.push(
navigatorkey.currentContext!,
MaterialPageRoute(
builder: (context) => HomeScreen(index: 2),
));
} else if (Provider.of<GeneralController>(navigatorkey.currentContext!,
listen: false)
.notification_name ==
'Restaurant' &&
Provider.of<GeneralController>(navigatorkey.currentContext!,
listen: false)
.notification_name !=
null) {
Provider.of<FoodController>(navigatorkey.currentContext!,
listen: false)
.restaurantDetailApi(navigatorkey.currentContext!,
id: details.payload);
}
},
onDidReceiveNotificationResponse: (details) {
print("details Data : ${details.payload}");
if (details.payload == 'Offer Section' && details.payload != null) {
if (tokenValue != null) {
Navigator.push(
navigatorkey.currentContext!,
MaterialPageRoute(
builder: (context) => OfferMeScreen(),
));
}
} else if (details.payload == 'Surprise Me Section' &&
details.payload != null) {
Navigator.push(
navigatorkey.currentContext!,
MaterialPageRoute(
builder: (context) => HomeScreen(index: 2),
));
} else if (Provider.of<GeneralController>(navigatorkey.currentContext!,
listen: false)
.notification_name ==
'Restaurant' &&
Provider.of<GeneralController>(navigatorkey.currentContext!,
listen: false)
.notification_name !=
null) {
Provider.of<FoodController>(navigatorkey.currentContext!,
listen: false)
.restaurantDetailApi(navigatorkey.currentContext!,
id: details.payload);
}
},
);
}
static void createanddisplaynotification(RemoteMessage message) async {
try {
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
const NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
"ikwteat_push_notification",
"ikwteatpushnotificationappchannel",
importance: Importance.max,
priority: Priority.high,
),
);
Map<String, dynamic> data = message.data;
String linkTo = data['notification_link_to'];
if (linkTo == 'Restaurant') {
print('object');
Provider.of<GeneralController>(navigatorkey.currentContext!,
listen: false)
.updateNotification(value: linkTo);
if (data['restaurant_id'] != null) {
linkTo = data['restaurant_id'];
print("Restaurant Id : ${linkTo}");
}
}
print('Notfication id ${id.runtimeType}');
await _notificationsPlugin.show(
id,
message.notification!.title,
message.notification!.body,
notificationDetails,
payload: linkTo,
);
} on Exception catch (e) {
print(e);
}
}
}
Here are the Firebase Function which Are handling different states of Notifications like Background , foreground and Terminated
void FirebaseFunctions() {
// 1. This method call when app in terminated state and you get a notification
// when you click on notification app open from terminated state and you can get notification data in this method
FirebaseMessaging.instance.getInitialMessage().then(
(message) {
print("FirebaseMessaging.instance.getInitialMessage");
if (message != null) {
print("New Notification");
}
},
);
FirebaseMessaging.onMessage.listen(
(message) {
print("FirebaseMessaging.oneMssage.listen");
if (message.notification != null) {
print(message.notification!.title);
print(message.notification!.body);
Map<String, dynamic> data = message.data;
String linkTo = data['notification_link_to'];
print("Link to ${linkTo}");
LocalNotificationService.createanddisplaynotification(message);
}
},
);
// 3. This method only call when App in background and not terminated(not closed)
FirebaseMessaging.onMessageOpenedApp.listen(
(message) {
print("FirebaseMessaging.onMessageOpenedApp.listen");
if (message.notification != null) {
print(message.notification!.title);
print(message.notification!.body);
// print("message.data22 ${message.data['_id']}");
LocalNotificationService.createanddisplaynotification(message);
}
},
);
}
when the Notification come it keeps giving me the Error like
D/CompatibilityChangeReporter( 2009): Compat change id reported: 160794467; UID 11103; state: ENABLED
E/MethodChannel#dexterous.com/flutter/local_notifications( 2009): Failed to handle method call
E/MethodChannel#dexterous.com/flutter/local_notifications( 2009): java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Integer.intValue()' on a null object reference
I couldn't understand where i am reading integer value if any body knows where i am going wrong please help me out
The only thing I can think of is that your app icon is not being present in the android/app/src/main/res/drawable folder or that it might be broken in any other way.
You can read more about it in a GitHub issue - it sums up as the following:
If you're using Android Studio right click on res folder in main
directory > Select new > Image Asset. Change Icon type to
'Notification Icons'. Set whatever clip art, image you want to set and
give it a name copy the name and pass that name to
AndroidInitializationSettings('app_icon') in place of 'app_icon'. This
will stop the crash because now the app has a legit icon for the
notifications.
Make sure to stop and rerun the app completely to apply the changes.
See here.
I'm working on a small app with GoogleSignIn-Auth. and stumbled upon a bug I cannot wrap my head around.
It seems like the fold of an Either seems to be skipped. It used to work before, when I had a complicated pile of blocs. Since I started reorganizing my widgets it started to this.
Future<Either<Failure, SignUpSuccess>> signInWithGoogle() async {
try {
final signUpSuccess = await googleRemoteDataSource.signInWithGoogle();
signUpSuccess.fold(
(failure) => () {
print("Got failure!");
return Left(GeneralFailure());
},
(success) => () {
return Right(signUpSuccess);
});
print("I skipped the fold!");
} catch (e) {
print("Caught exception!");
return Left(GeneralFailure());
}
print("Instant fail!");
return Left(GeneralFailure());
}
I have a widget that's listening to a SignInBloc emitting the states:
class SignUpRoot extends StatelessWidget {
SignUpRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
create: (context) => sl<SignInBloc>(),
child: BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if (state is SignInWithGoogleLoaded) {
// Navigate to SignInNamePage
print("This seems to work!");
} else if (state is SignInWithGoogleLoading) {
// Navigate to loading page
print("Loading Google...");
} else if (state is SignInError) {
// Navigate to error page
print("An error occured while signing in!");
}
},
child: const SignUpMainPage(),
)));
}
And last but not least my bloc:
class SignInBloc extends Bloc<SignInEvent, SignInState> {
final SignUpUseCases useCases;
SignInBloc({required this.useCases}) : super(SignInInitial()) {
on<SignInWithGooglePressed>((event, emit) async {
// Show Loading indicator
emit(SignInWithGoogleLoading());
// wait for sign in response
Either<Failure, SignUpSuccess> successOrFailure =
await useCases.signInWithGoogle();
// emit corresponding state
successOrFailure.fold(
(failure) => emit(SignInError()),
(success) => () {
// emit sign in loaded state
emit(SignInWithGoogleLoaded());
// create new (local) user
// assign user data e.g. display name
});
});
}
}
Thanks for any help!
The problem is that the fold method returns the value of the left or right functions.
https://pub.dev/documentation/dartz/latest/dartz/Either/fold.html
B fold<B>(
B ifLeft(
L l
),
B ifRight(
R r
)
)
Your code should be corrected to:
Future<Either<Failure, SignUpSuccess>> signInWithGoogle() async {
try {
final signUpSuccess = await googleRemoteDataSource.signInWithGoogle();
return signUpSuccess.fold(
(failure) => () {
return Left(GeneralFailure());
},
(success) => () {
return Right(signUpSuccess);
});
} catch (e) {
return Left(GeneralFailure());
}
return Left(GeneralFailure());
}
I just added the return at the start of the fold, now the value returned from left or right will be returned by your function.
You just have to remove the arrow in the success part of the fold.
Future<Either<Failure, SignUpSuccess>> signInWithGoogle() async {
try {
final signUpSuccess = await googleRemoteDataSource.signInWithGoogle();
signUpSuccess.fold(
(failure) => () {
print("Got failure!");
return Left(GeneralFailure());
},
(success){
return Right(signUpSuccess);
});
print("I skipped the fold!");
} catch (e) {
print("Caught exception!");
return Left(GeneralFailure());
}
print("Instant fail!");
return Left(GeneralFailure());
}
class SignInBloc extends Bloc<SignInEvent, SignInState> {
final SignUpUseCases useCases;
SignInBloc({required this.useCases}) : super(SignInInitial()) {
on<SignInWithGooglePressed>((event, emit) async {
// Show Loading indicator
emit(SignInWithGoogleLoading());
// wait for sign in response
Either<Failure, SignUpSuccess> successOrFailure =
await useCases.signInWithGoogle();
// emit corresponding state
successOrFailure.fold(
(failure) => emit(SignInError()),
(success) {
emit(SignInWithGoogleLoaded());
});
});
}
}
I have a form to create/update activity which is a stateful widget like this:
class ActivityForm extends StatefulWidget {
final Activity activity;
final bool isEdit;
const ActivityForm({Key key, this.activity, this.isEdit}) : super(key: key);
#override
_ActivityFormState createState() => _ActivityFormState();
}
class _ActivityFormState extends State<ActivityForm> {
final _formKey = GlobalKey<FormState>();
List<int> activityDetailIdToBeDeleted;
...
void submitHandler() async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
Navigator.pop(context);
}
in my form there are some detail list of activities. When i press delete it adds the id of
the activity detail to the list of "to be deleted" :
IconButton(
icon: Icon(Icons.delete),
onPressed: () => removeRincianPekerjaan(
activityDetail.description),),
here's the function:
void removeRincianPekerjaan(desc) {
if (widget.isEdit) {
int detailId = rincianPekerjaanList
.where((element) => element.description == desc)
.first
.id;
if (!activityDetailIdToBeDeleted.contains(detailId))
activityDetailIdToBeDeleted.add(detailId);
print(activityDetailIdToBeDeleted);
}
setState(() {
rincianPekerjaanList
.removeWhere((element) => element.description == desc);
});
}
that list of ids will be sent to provider which looks like this:
Future<bool> updateActivity(
Activity activity, List<int> activityDetailToBeDeleted) async {
final response = await ActivityService()
.updateActivity(activity.id.toString(), activity.toMap());
if (response != null) {
// Update Success.
if (response.statusCode == 200) {
var body = json.decode(response.body);
removeActivityToList(activity);
activity = Activity.fromMap(body['activity']);
addActivityToList(activity);
if (activityDetailToBeDeleted.isNotEmpty) {
print("to be deleted is not empty.");
print("ID to be deleted: ${activityDetailToBeDeleted.join(",")}");
final res = await ActivityService()
.deleteActivityDetail(activityDetailToBeDeleted.join(","));
if (res.statusCode == 204) {
print('ID DELETED.');
return true;
}
}
return true;
}
// Error unauthorized / token expired
else if (response.statusCode == 401) {
var reauth = await AuthProvider().refreshToken();
if (reauth) {
return await createActivity(activity);
} else {
return Future.error('unauthorized');
}
}
return Future.error('server error');
}
return Future.error('connection failed');
}
the problem is if the list is empty, or i didn't add the id to the list, builder context is not null but if i add an id to the list "to be deleted", the Navigator.pop(context) will throw an error because context is null. I don't understand why it becomes null.
full code in here
Your [context] must came from the build() method. Like this.
void submitHandler(BuildContext context) async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
});
}
I've been trying a sample Flutter application code from GitHub to simply login and register the user on Firebase. Every time I login or register after clean building the application, it takes me to the main page but throws this exception Exception has occurred. NoSuchMethodError (NoSuchMethodError: Class 'FlutterError' has no instance getter 'code'. Receiver: Instance of 'FlutterError' Tried calling: code)
I've no idea what 'FlutterError' is referring to because I don't see any such class. and there are two occurrences of code in the file named 'login-register.dart'. I'm attaching the code below:
(Note: it runs okay after I hot reload the app and the user is already logged in, only throws exception the first time)
void _validateLoginInput() async {
final FormState form = _formKey.currentState;
if (_formKey.currentState.validate()) {
form.save();
_sheetController.setState(() {
_loading = true;
});
try {
final FirebaseUser user = (await FirebaseAuth.instance
.signInWithEmailAndPassword(email: _email, password: _password)).user;
// final uid = user.uid;
Navigator.of(context).pushReplacementNamed('/home');
} catch (error) {
switch (error.code) {
case "ERROR_USER_NOT_FOUND":
{
_sheetController.setState(() {
errorMsg =
"There is no user with such entries. Please try again.";
_loading = false;
});
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Container(
child: Text(errorMsg),
),
);
});
}
break;
case "ERROR_WRONG_PASSWORD":
{
_sheetController.setState(() {
errorMsg = "Password doesn\'t match your email.";
_loading = false;
});
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Container(
child: Text(errorMsg),
),
);
});
}
break;
default:
{
_sheetController.setState(() {
errorMsg = "";
});
}
}
}
} else {
setState(() {
_autoValidate = true;
});
}
}
void _validateRegisterInput() async {
final FormState form = _formKey.currentState;
if (_formKey.currentState.validate()) {
form.save();
_sheetController.setState(() {
_loading = true;
});
try {
final FirebaseUser user = (await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: _email, password: _password)).user;
UserUpdateInfo userUpdateInfo = new UserUpdateInfo();
userUpdateInfo.displayName = _displayName;
user.updateProfile(userUpdateInfo).then((onValue) {
Navigator.of(context).pushReplacementNamed('/home');
Firestore.instance.collection('users').document().setData(
{'email': _email, 'displayName': _displayName}).then((onValue) {
_sheetController.setState(() {
_loading = false;
});
});
});
} catch (error) {
switch (error.code) {
case "ERROR_EMAIL_ALREADY_IN_USE":
{
_sheetController.setState(() {
errorMsg = "This email is already in use.";
_loading = false;
});
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Container(
child: Text(errorMsg),
),
);
});
}
break;
case "ERROR_WEAK_PASSWORD":
{
_sheetController.setState(() {
errorMsg = "The password must be 6 characters long or more.";
_loading = false;
});
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Container(
child: Text(errorMsg),
),
);
});
}
break;
default:
{
_sheetController.setState(() {
errorMsg = "";
});
}
}
}
} else {
setState(() {
_autoValidate = true;
});
}
}
The exception you're catching doesn't have a code property. That only exists with the firebase exception implementation, not the general exception class.
If you expect a certain type of error, you should explicitly catch that error and handle it properly and have a separate catch block for all other errors.
This can be done with an on ... catch block:
try {
final FirebaseUser user = (await FirebaseAuth.instance
.signInWithEmailAndPassword(email: _email, password: _password)).user;
// final uid = user.uid;
Navigator.of(context).pushReplacementNamed('/home');
} on FirebaseAuthException catch (error) {
...
} catch(e) {
...
}
The methods you're calling in the code you shared will throw FirebaseAuthExceptions as shown in the code above.
You are getting an error that is not a FirebaseError but a FlutterError. This means, it does not implement a code field.
You can simply put
if(!(error is FirebaseError)){
print(error.message); // this is the actual error that you are getting
}
right below catch(error) { (in both files) to handle this.
However, it seems like you get another Flutter Error that you might want to handle. It should be printed to the console now.