I want to navigate specific page when user click fcm notification. Problem is navigator not work on callback method.
My device received push noti well.
I added FLUTTER_NOTIFICATION_CLICK on noti body. And there's no missing data.
Callback method run well, but only Navigator and showDialog not work. So I guess problem is related with context.
// main.dart
void main() {
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => _AppState();
}
class _AppState extends State<MyApp> {
final FirebaseMessaging _fm = new FirebaseMessaging();
final GlobalKey<NavigatorState> navigatorKey =
GlobalKey(debugLabel: 'Main Navigator');
final routes = {
// skip on question
};
fcmSetting(context) {
_fm.configure(
onLaunch: (Map<String, dynamic> message) {
pushTo(message); // not work
},
onMessage: (Map<String, dynamic> message) {
print('onMessage $message'); // it run well
onMessageSend(message); // but it's not
},
onResume: (Map<String, dynamic> message) {
pushTo(message); // not work
},
);
_fm.requestNotificationPermissions(
const IosNotificationSettings(sound: true, badge: true, alert: true));
_fm.getToken().then((token) {
DBFactory.getInstance().insert('fcmToken', token);
});
}
pushTo(message) {
if (message['type'] == 'notice') {
Navigator.of(navigatorKey.currentContext).push(
MaterialPageRoute(builder: (_) => NoticeDetailScreen(message['id'])));
}
}
onMessageSend(message) {
showDialog(
context: context,
builder: (bd) => new AlertDialog(
title: LText('메세지 도착!'),
content: LText('$message'),
actions: <Widget>[
LFlatButton(
text: '확인', onPressed: () => Navigator.of(bd).pop()),
],
));
}
#override
Widget build(BuildContext context) {
fcmSetting(context);
return new MaterialApp(
navigatorKey: navigatorKey,
title: 'App title',
initialRoute: '/',
routes: routes,
);
}
}
I tried already Navigator.of(navigatNavigator.of(navigatorKey.currentContext).push(...)orKey.currentContext).push(...) or Navigator.of(context).push(...). But not work.
How do I solve it?
You have to move the code from the App file into a widget. I would say create a home page and perform the navigation in there, make the home page the child of your MaterialApp and register the callbacks in the home view.
I haven't figured out why the context there doesn't work, but I experienced the same thing recently and had to move navigation into a view deeper down the line.
Related
I'm rebuilding an iOS app using Flutter and the flow is as followed:
Everytime the user lands on the homepage, the user data is reloaded from the backend to check if anything has changed.
The way I achieve this in Swift / iOS is by using the viewDidLoad() function.
My Flutter code is like this:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
User user = User();
#override
void dispose() {
super.dispose();
}
#override
void initState() {
super.initState();
_fetchData(context);
}
#override
Widget build(BuildContext context) {
return Container(
color: RColor.COLOR_main,
child: Column(
children: [
Container(
height: MediaQuery.of(context).size.height / 7,
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(20),
child: Container(
child: Text("This is the homepage"),
alignment: Alignment.bottomCenter,
),
),
],
));
}
Future _fetchData(BuildContext context) async {
_fetchUserAPI(context);
}
Future _fetchUserAPI(BuildContext context) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token");
var url = RConstants.API_BASE_URL + "/v1/users/self";
Response response = await Dio()
.get(url, options: Options(headers: {"Authorization": accessToken}));
setState(() {
user = User.fromJson(response.data);
});
} catch (e) {
print(e.toString());
Alert(
context: context,
title: "Something Went Wrong",
desc: "Something went wrong while fetching your user data",
type: AlertType.error)
.show();
}
}
}
void initState() however, doesn't get triggered each time the user lands on the homepage. What is the correct way to achieve this?
Well, it really depends on what exactly you mean by "Everytime the user lands on the homepage".
If user navigates from the HomePage to some other view via Navigator.push and then goes back via Navigator.pop then the HomePage's state remain the same and of course the initState method does not trigger.
If you want to get notified on the HomePage if the route above it in the navigator gets popped then you need to use this method, override it and then inside it you will be able to call the _fetchData() and update the homepage's state.
One more thing: when you have some async call like _fetchData() it is a wrong pattern to just invoke it inside the initState() or any other framework methods. Because it will be invoked and the build() method of your state will almost always be invoked immediately before the result of async call will come back. The correct way to handle such situations is to use the FutureBuilder widget.
If "Everytime the user lands on the homepage" means something else, like e.g. the app was in the background and gets brought foreground or when there is support for push notifications implemented and users click on the notification and the app is opened - such cases also can be handled but that is a broader topic.
RouteAware can help.
define final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>(); in main.dart.
set navigatorObservers: [ routeObserver ], in MaterialApp function.
mixin RouteAware in your page that need achieve viewWillAppear feature.
override didChangeDependencies and didPopNext methods and subscribe this page to routeObserver.
For example:
main.dart
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
void main() {
runApp(MaterialApp(
home: HomePage(),
navigatorObservers: [ routeObserver ],
));
}
home_page.dart
class _HomePageState extends State<HomePage> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPopNext() {
super.didPopNext();
debugPrint("viewWillAppear");
}
}
I implemented the Quick_actions plugin in my project and i want to open a specific screen but in the quickaction handler function the navigator doesnt work. whit a Try-Catch, the exception shows that the context showld be from a navigator, but im using the context of the navigatorKey of my MaterialApp.
if i put any other function like a print('some text') it works, the problem only happend when I try to use the navigator
Create the quick actions and add the handler function
createQuickActions() {
quickActions.initialize(
(String shortcutId) {
switch (shortcutId) {
case 'settings':
try {
Navigator.push(
MyApp.navigatorKey.currentContext,
MaterialPageRoute(
builder: (context) => SettingsScreen(sistemas),
),
);
} catch (e) {
print(e);
}
print('selected: $shortcutId');
break;
}
}
);
}
Initialice the quick actions
quickActions.setShortcutItems(
<ShortcutItem>[
const ShortcutItem(
type: 'settings',
localizedTitle: 'settings',
icon: 'settings',
),
],
);
All this code its in my SplashScreen because the plugin's documentation says that should be in an early state of the app
I expect that the app open the settings screen and print 'settings' but it opens the main screen and print 'settings' if the app its already open, but if its not it tries to open something and then close itself (not force close message)
In the following example,
Use MainView in quick action will open Login widget and directly click app will open Home widget
You can reference https://www.filledstacks.com/snippet/managing-quick-actions-in-flutter/ for detail
full code
import 'package:flutter/material.dart';
import 'package:quick_actions/quick_actions.dart';
import 'dart:io';
class QuickActionsManager extends StatefulWidget {
final Widget child;
QuickActionsManager({Key key, this.child}) : super(key: key);
_QuickActionsManagerState createState() => _QuickActionsManagerState();
}
class _QuickActionsManagerState extends State<QuickActionsManager> {
final QuickActions quickActions = QuickActions();
#override
void initState() {
super.initState();
_setupQuickActions();
_handleQuickActions();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
void _setupQuickActions() {
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'action_main',
localizedTitle: 'Main view',
icon: Platform.isAndroid ? 'quick_box' : 'QuickBox'),
ShortcutItem(
type: 'action_help',
localizedTitle: 'Help',
icon: Platform.isAndroid ? 'quick_heart' : 'QuickHeart')
]);
}
void _handleQuickActions() {
quickActions.initialize((shortcutType) {
if (shortcutType == 'action_main') {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Login()));
} else if(shortcutType == 'action_help') {
print('Show the help dialog!');
}
});
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QuickActions Demo', home: QuickActionsManager(child: Home()));
}
}
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Home')));
}
}
class Login extends StatelessWidget {
const Login({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Login')));
}
}
I`m using local notification plugin and everything works fine except that when i tap on the notification.i want to navigate to specific screen when the user tap the notification
Future onSelectNotification(String payload) async {
//convert payload json to notification model object
try{
Map notificationModelMap = jsonDecode(payload);
NotificationModel model = NotificationModel.fromJson(notificationModelMap);
await Navigator.push(
context,// it`s null!
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
}
but the context always null and gives me an exception
NoSuchMethodError: The method 'ancestorStateOfType' was called on null.
Edit
i tried to use navigatorKey and pass it to material app as suggested by #Günter Zöchbauer but it gives me another exception
Navigator operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static final navigatorKey = new GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey:navigatorKey ,
title: 'notification',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: new RootPage(auth: new Auth(),),
);
}
}
ReminderPage
class ReminderPage extends StatefulWidget {
#override
_ReminderPageState createState() => _ReminderPageState();
}
class _ReminderPageState extends State<ReminderPage> {
final flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
....
#override
void initState() {
super.initState();
_initLocalNotification();
....
}
// this method is called when u tap on the notification
Future onSelectNotification(String payload) async {
//convert payload json to notification model object
Map notificationModelMap = jsonDecode(payload);
NotificationModel model = NotificationModel.fromJson(notificationModelMap);
try{
await Navigator.push(
MyApp.navigatorKey.currentState.context,
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
}
}
Edit 2
instead of using
await Navigator.push(
MyApp.navigatorKey.currentState.context,
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
i used
await MyApp.navigatorKey.currentState.push(MaterialPageRoute(builder: (context) => CommitmentPage(model)));
and it worked fine, but when i tap on the notification after killing the app, it takes me to the home page not to the desired one!
i think navigatorKey has not been initialized yet!
1. FMC payload
{
"notification": {
"body": "Hey, someone book your product",
"title": "production booking"
},
"priority" : "high",
"data": {
"action" : "BOOKING" //to identify the action
},
"to": "deviceFCMId"
}
2. Set the payload data in localnotification.show method
showNotification(RemoteMessage message) {
RemoteNotification notification = message.notification;
AndroidNotification android = message.notification?.android;
String action = message.data['action']; // get the value set in action key from FCM Json in Step1
// local notification to show to users using the created channel.
if (notification != null && android != null) {
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: androidNotificationDetails, iOS: iOSNotificationDetails),
payload: action // set the value of payload
);
}
}
3. Create a navigatorKey in MyApp class
class _MyAppState extends State<MyApp> {
final GlobalKey<NavigatorState> navigatorKey =
GlobalKey(debugLabel: "Main Navigator"); //
}
4. Set navigatorkey on your MaterialApp
#override
Widget build(BuildContext context) {
//this change the status bar color to white
return MaterialApp(
navigatorKey: navigatorKey,
....
5. on initState, init localnotificationPlugin and declare the onSelectNotification method
#override
void initState() {
super.initState();
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
}
Future<dynamic> onSelectNotification(payload) async {
// implement the navigation logic
}
6. Navigation logic example
Future<dynamic> onSelectNotification(payload) async {
// navigate to booking screen if the payload equal BOOKING
if(payload == "BOOKING"){
this.navigatorKey.currentState.pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => BookingScreen()),
(Route<dynamic> route) => false,
);
}
}
Pass a navigatorKey to MaterialApp and use this key to get the context. This context contains a Navigator and you can use it to switch to whatever page you want.
https://docs.flutter.io/flutter/material/MaterialApp/navigatorKey.html
With the latest change you've done, you being taken to the home page, is more to do with the fact that you instantiate and initialise the plugin a couple of pages further in your app. considering the app got killed, the plugin would've been killed too. you should look into initialising the plugin closer to when your app starts e.g. around when the app is created or when the first page is created, depending on the behaviour you want. In this issue https://github.com/MaikuB/flutter_local_notifications/issues/99, another dev has been able to change the first page shown if the app is killed but that may different to what you want to happen.
Is it possible to navigate to specific MaterialPageRoute in app from notification click? I have notifications configured in the main screen:
void _configureNotifications() {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
_firebaseMessaging.requestNotificationPermissions();
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) {
_goToDeeplyNestedView();
},
onLaunch: (Map<String, dynamic> message) {
_goToDeeplyNestedView();
},
onResume: (Map<String, dynamic> message) {
_goToDeeplyNestedView();
},
);
}
_goToDeeplyNestedView() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DeeplyNestedView()));
}
The problem is, that when i configure it like this, it woks only from Widget where i configured notifications (I guess it's because of using 'context' in
Navigator.push(). Is there some way to access to MaterialPageRoute from anywhere in the app without using any context?
Thank you in advance for your answers.
There aren't very many cases where a GlobalKey is a good idea to use, but this might be one of them.
When you build your MaterialApp (which I assume you're using), you can pass in a navigatorKey parameter which specifies the key to use for the navigator. You can then use this key to access the navigator's state. That would look something like this:
class _AppState extends State<App> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: "Main Navigator");
#override
Widget build(BuildContext context) {
return new MaterialApp(
navigatorKey: navigatorKey,
home: new Scaffold(
endDrawer: Drawer(),
appBar: AppBar(),
body: new Container(),
),
);
}
}
And then to use it you access the navigatorKey.currentContext:
_goToDeeplyNestedView() {
navigatorKey.currentState.push(
MaterialPageRoute(builder: (_) => DeeplyNestedView())
);
}
Initialize navigatorState globalkey
final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: "Main Navigator");
navigatorKey.currentState.push(
MaterialPageRoute(builder: (_) => classname())
);
I was facing the same issue and fixed it using the below code :
Creating 1 global key like :
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
Assigning the key to Scaffold in build method :
return Scaffold( key: _scaffoldKey,
appBar: Container(), body:
Container() );
printing ('something') before this to make sure your app is reaching to below code:
Navigator.push(
_scaffoldKey.currentContext,
MaterialPageRoute(
builder: ( _) =>
NotificationDetailEn(notificationModel: model)));
I think it will be helpful.
Let's say, I have a test for a screen in Flutter using WidgetTester. There is a button, which executes a navigation via Navigator. I would like to test behavior of that button.
Widget/Screen
class MyScreen extends StatefulWidget {
MyScreen({Key key}) : super(key: key);
#override
_MyScreenState createState() => _MyScreenScreenState();
}
class _MyScreenState extends State<MyScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.of(context).pushNamed("/nextscreen");
},
child: Text(Strings.traktTvUrl)
)
)
);
}
}
Test
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: MyScreen()));
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
//how to test navigator?
});
}
I there a proper way how to check, that Navigator was called? Or is there a way to mock and replace navigator?
Pleas note, that code above will actually fail with an exception, because there is no named route '/nextscreen' declared in application. That's simple to solve and you don't need to point it out.
My main concern is how to correctly approach this test scenario in Flutter.
While what Danny said is correct and works, you can also create a mocked NavigatorObserver to avoid any extra boilerplate:
import 'package:mockito/mockito.dart';
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
That would translate to your test case as follows:
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: MyScreen(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
/// Verify that a push event happened
verify(mockObserver.didPush(any, any));
/// You'd also want to be sure that your page is now
/// present in the screen.
expect(find.byType(DetailsPage), findsOneWidget);
});
}
I wrote an in-depth article about this on my blog, which you can find here.
In the navigator tests in the flutter repo they use the NavigatorObserver class to observe navigations:
class TestObserver extends NavigatorObserver {
OnObservation onPushed;
OnObservation onPopped;
OnObservation onRemoved;
OnObservation onReplaced;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPushed != null) {
onPushed(route, previousRoute);
}
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPopped != null) {
onPopped(route, previousRoute);
}
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onRemoved != null)
onRemoved(route, previousRoute);
}
#override
void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
if (onReplaced != null)
onReplaced(newRoute, oldRoute);
}
}
This looks like it should do what you want, however it may only work form the top level (MaterialApp), I'm not sure if you can provide it to just a widget.
Inspired by the other posts, this is my 2022 null-safe Mockito-based approach. Imagine I have this helper method I want to unit test:
navigateToNumber(int number, BuildContext context) {
Navigator.of(context).pushNamed(
number.isEven ? '/even' : '/odd'
);
}
It can be tested this way:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:my_app/number_route_helper.dart';
import 'number_route_helper_test.mocks.dart';
#GenerateMocks([],
customMocks: [
MockSpec<NavigatorObserver>(returnNullOnMissingStub: true)
])
void main() {
group('NumberRouteHelper', () {
testWidgets('navigateToNumber', (WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
// "Fake" routes used to verify the right route was pushed
final evenRoute = MaterialPageRoute(builder: (_) => Container());
final oddRoute = MaterialPageRoute(builder: (_) => Container());
await tester.pumpWidget(
MaterialApp(
home: Container(),
navigatorObservers: [mockObserver],
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/even':
return evenRoute;
case '/odd':
return oddRoute;
}
}
),
);
final BuildContext context = tester.element(find.byType(Container));
/// Verify that a push to evenRoute happened
navigateToNumber(2, context);
await tester.pumpAndSettle();
verify(mockObserver.didPush(evenRoute, any));
/// Verify that a push to oddRoute happened
navigateToNumber(3, context);
await tester.pumpAndSettle();
verify(mockObserver.didPush(oddRoute, any));
});
});
}
Just remember you need to have Mockito installed, as described here: https://pub.dev/packages/mockito
This is modified version of the other answer to show how to do it with mocktail instead of mockito:
import 'package:mocktail/mocktail.dart';
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
class FakeRoute extends Fake implements Route {}
void main() {
setUpAll(() {
registerFallbackValue(FakeRoute());
});
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: MyScreen(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
verify(mockObserver.didPush(any(), any()));
expect(find.byType(DetailsPage), findsOneWidget);
});
}
Following solution is, let's say, a general approach and it's not specific to Flutter.
Navigation could be abstracted away from a screen or a widget. Test can mock and inject this abstraction. This approach should be sufficient for testing such behavior.
There are several ways how to achieve that. I will show one of those, for purpose of this response. Perhaps it's possible to simplify it a bit or to make it more "Darty".
Abstraction for navigation
class AppNavigatorFactory {
AppNavigator get(BuildContext context) =>
AppNavigator._forNavigator(Navigator.of(context));
}
class TestAppNavigatorFactory extends AppNavigatorFactory {
final AppNavigator mockAppNavigator;
TestAppNavigatorFactory(this.mockAppNavigator);
#override
AppNavigator get(BuildContext context) => mockAppNavigator;
}
class AppNavigator {
NavigatorState _flutterNavigator;
AppNavigator._forNavigator(this._flutterNavigator);
void showNextscreen() {
_flutterNavigator.pushNamed('/nextscreen');
}
}
Injection into a widget
class MyScreen extends StatefulWidget {
final _appNavigatorFactory;
MyScreen(this._appNavigatorFactory, {Key key}) : super(key: key);
#override
_MyScreenState createState() => _MyScreenState(_appNavigatorFactory);
}
class _MyScreenState extends State<MyScreen> {
final _appNavigatorFactory;
_MyScreenState(this._appNavigatorFactory);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
_appNavigatorFactory.get(context).showNextscreen();
},
child: Text(Strings.traktTvUrl)
)
)
);
}
}
Example of a test (Uses Mockito for Dart)
class MockAppNavigator extends Mock implements AppNavigator {}
void main() {
final appNavigator = MockAppNavigator();
setUp(() {
reset(appNavigator);
});
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: MyScreen(TestAppNavigatorFactory())));
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
verify(appNavigator.showNextscreen());
});
}