How to navigate without context in flutter app? - flutter

I have an app that recieves push notification using OneSignal. I have made a notification opened handler that should open specific screen on click of the notification. How can i navigate to a screen without context. or how can I open specific screen on app startup. My code:
OneSignal.shared.setNotificationOpenedHandler((notification) {
var notify = notification.notification.payload.additionalData;
if (notify["type"] == "message") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DM(user: notify['id']),
),
);
}
if (notify["type"] == "user") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Profileo(notify["id"]),
),
);
}
if (notify["type"] == "post") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewPost(notify["id"]),
),
);
}
});
I am able to achieve this when the app is opened for the first time but It only opens the homepage If i close the app and even if I re-open it. I guess that is because the context is changed.
Please Help!!

Look at this here:
https://github.com/brianegan/flutter_redux/issues/5#issuecomment-361215074
You can set a global key for your navigation:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Pass it to MaterialApp:
new MaterialApp(
title: 'MyApp',
onGenerateRoute: generateRoute,
navigatorKey: navigatorKey,
);
Push routes:
navigatorKey.currentState.pushNamed('/someRoute');

You can use this wonderful plugin:
https://pub.dev/packages/get
Description from the package: A consistent navigation library that lets you navigate between screens, open dialogs, and display snackbars from anywhere in your code without context.
Get.to(NextScreen()); // look at this simplicity :)
Get.back(); // pop()
Get.off(NextScreen()); // clears the previous routes and opens a new screen.

This solution is general if you want to navigate or to show dialog without context using globalKey especially with Bloc or when your logic is separated from your UI part.
Firstly install this package:
Not: I'm using null safety version
get_it: ^7.2.0
Then create a separate file for your service locator:
service_location.dart
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!.pushNamed(routeName);
}
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
));
}
}
on main.dart:
void main() {
WidgetsFlutterBinding.ensureInitialized();
NavigationService().setupLocator();
runApp(MyApp());
}
// add navigatorKey for MaterialApp
MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
),
at your business logic file bloc.dart
define this inside the bloc class or at whatever class you want to use navigation inside
Then start to navigate inside any function inside.
class Cubit extends Cubit<CubitState> {
final NavigationService _navigationService = locator<NavigationService>();
void sampleFunction(){
_navigationService.navigateTo('/home_screen'); // to navigate
_navigationService.showMyDialog(); // to show dialog
}
}
Not: I'm using generateRoute for routing.

Quickest fix is above using global navigatorKey (like #tsdevelopment answered).
To fix undefined navigatorKey, it must be imported from where it is instantiated (for this example in main.dart).
Your main.dart
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(CupertinoApp(
title: 'Navigate without context',
initialRoute: '/',
navigatorKey: navigatorKey, // important
onGenerateRoute: ...
));
}
For example you are in your lib/utils/api.dart
import 'package:your_package_name/main.dart'; // important
abstract class API {
static Future<dynamic> get() async {
// call some api
...
// then you want to navigate to specific screen like login
navigatorKey.currentState?.pushNamed('/login'); // navigate to login, with null-aware check
}
}
Also have a gist example if you prefer in a service approach.
Check this: https://gist.github.com/josephdicdican/81e59fad70530eac251ad6c28e2dcd4b

I know this is an old post, but there is a package that handles navigation without the build context (Using a navigator key) called flutter_navigator: https://pub.dev/packages/flutter_navigator
It allows you to navigate something like this:
_flutterNavigation.push(//YourRoute);
Everything seems to be mapped 1:1 with Flutter's Navigator API, so there is no worries there!

You can use this no_context_navigation package
as the name suggests, we can navigate without context
navService.pushNamed('/detail_screen', args: 'From Home Screen');

Related

Flutter: How to pass data between screens?

How can I change the visibility of a button on screen "X" from a button on screen "Y".
One popular approach (using the provider architecture) would be something like this:
Define a provider that handles all the logic and holds your data:
class MyProvider extends ChangeNotifier {
bool showMyButton = false;
MyProvider() {}
void showButton() {
showMyButton = true;
// This line notifies all consumers
notifyListeners();
}
void refresh() {
notifyListeners();
}
}
To access the provider everywhere you need to register it:
void main() => runApp(
// You can wrap multiple providers like this
MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (_) => MyProvider()),
],
child: const MyApp(),
),
);
On the button that you want to control you can use a Consumer to listen to the providers values:
Consumer<MyProvider>(builder: (_, model, __) {
return Visibility(
visible: model.showMyButton,
child: MaterialButton(...),
);
})
Now in your second screen you can access that provider with:
Provider.of<MyProvider>(context, listen: false)
.showButton();
However you might have to call notifyListener one more time when returning from screen Y to screen X:
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenY()));
Provider.of<MyProvider>(context, listen: false).refresh();
Keep in mind that there is a lot more to provider so please have a look at their official docs.
Also be aware of the fact that there are easier ways to just pass data between screens but you will often arrive at a point where you will need a better way of managing state and provider provides just that ;)
You can pass the data via push and pop of navigation. Or else use ChangeNotifier class to notify the state of button easily.

How do I set up navigator using Getx and Auto Route?

Problem:
I am having trouble setting up navigation using GetX and AutoRoute.
Code Setup:
According to the GetX documentation, if you want to use GetX navigation you have to replace MaterialApp() with GetMaterialApp(). You also set the routes.
void main() {
runApp(
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyHomePage()),
GetPage(name: '/second', page: () => Second()),
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.zoom
),
],
)
);
}
The AutoRoute example uses MaterialApp.router() to set up the routerDelegate and routeInformationParser.
final _appRouter = AppRouter()
...
Widget build(BuildContext context){
return MaterialApp.router(
routerDelegate: _appRouter.delegate(...initialConfig),
routeInformationParser: _appRouter.defaultRouteParser(),
),
}
Here is how I set up the navigation according to Getx and AutoRoute:
void main() {
configureDependencies();
runApp(Portfolio());
}
class Portfolio extends StatelessWidget {
final _appRouter = AppRouter.Router();
#override
Widget build(BuildContext context) {
return GetMaterialApp.router(
routerDelegate: _appRouter.delegate(),
routeInformationParser: _appRouter.defaultRouteParser(),
builder: (context, extendedNav) => Theme(
data: ComplexReduxTheme.complexReduxLightTheme,
child: extendedNav ?? Container(color: Colors.red),
),
);
}
}
I am using GetMaterialApp.router which returns a GetMaterialApp. Despite this, I get the error "You are trying to use contextless navigation without a GetMaterialApp or Get.key.". I have tried setting up the navigator key and setting Get.testMode = true but nothing happens(no error) when I try to navigate to another screen.
Desired Result:
I should be able to navigate to the desired screen via Get.toNamed().
Current Result:
I get the following error from GetX when trying to navigate to another screen using Get.toNamed() : "You are trying to use contextless navigation without
a GetMaterialApp or Get.key.
If you are testing your app, you can use:
[Get.testMode = true], or if you are running your app on
a physical device or emulator, you must exchange your [MaterialApp]
for a [GetMaterialApp]."
AutoRoute Version: 2.2.0
Get Version: 4.1.4
You don't need external routing plugin, GetX already did that for you, and if you want to navigate, just use Get.toNamed("/some-page") and it will show you the page you wanted. Same goes to nested route.
For Example
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.zoom,
children: [
GetPage(
name: '/child-of-third',
page: () => ChildOfThird(),
),
],
),
// You access it like this
Get.toNamed("/third");
// And this one, for the nested page
Get.toNamed("/third/child-of-third");
The reason you got the error is when you use external routing plugin in GetX, it will generate their own code, with their own context in their own ecosystem. GetX doesn't know which context does the plugin use since it was outside of its lifecycle.
I was facing the same issue when combining both getx and auto router in my case i needed nested navigation as well I created a work around like this
I created initial bindings and passed appRouter to it and saved it in getx routing controller that i was using to a method like Get.toNamed because with initial appRouter you don't need context you can navigate like this
// main app widget
class _myAppState extends State<MyApp> {
final _appRouter = AppRouter();
#override
Widget build(BuildContext context) {
return GetMaterialApp.router(
routerDelegate: _appRouter.delegate(),
routeInformationParser: _appRouter.defaultRouteParser(),
initialBinding: InitialBinding(router: _appRouter,),
);
}
}
// initial binding to store to store app router
class InitialBinding extends Bindings {
AppRouter router;
InitialBinding({required this.router,});
#override
void dependencies() {
Get.put(NavRoutesController(router: router,),permanent: true);
}
}
// router controller
class NavRoutesController extends GetxController {
AppRouter router;
NavRoutesController({required this.router,});
void toNamed(String route){
router.pushNamed(route);
}
}
//to navigate use
final router = Get.find<RouterController>();
router.toNamed("/some")
//or
Get.find<RouterController>().toNamed("/some")
// you can get base context as well from AppRouter like this
Get.find<RouterController>().router.navigatorKey.currentState.context

Flutter navState context

I searched for a way to navigate through notification and i had to find solution for using context out of widget scoped so i created Global variable and Navigator state and used it, it worked fine.
Now i'm thinking why i have to pass every route push the context of the widget, why not use the navigator state on every route push.
import 'package:flutter/material.dart';
/// Global variables
/// * [GlobalKey<NavigatorState>]
class GlobalVariable {
/// This global key is used in material app for navigation through firebase notifications.
/// [navState] usage can be found in [notification_notifier.dart] file.
static final GlobalKey<NavigatorState> navState = GlobalKey<NavigatorState>();
}
adding to the MaterialApp
MaterialApp(
title: 'myapp',
navigatorKey: GlobalVariable.navState,
Navigator.push(GlobalVariable.navState.currentContext, MaterialPageRoute(builder: (BuildContext context) => RoutePage(child: ContactForm())));
instead of
(context) => Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => RoutePage(child: ContactForm())));

Flutter web url navigation

I would like to know how can I navigate to a URL in my Flutter web app.
Currently am using the Navigator.of(context).push(MaterialPageRoute(...)); and I only get localhost:5354/#/ in the address bar.
Also I would like to know how I can I navigate to a particular URL directly by just pasting the URL into the browser's addresses bar.
You need to use named routes instead of directly using classes to routes.
You can use this package named fluro https://pub.dev/packages/fluro
or else you can use default navigation that flutter provides.
with fluro you can do something like this
main.dart
import '../routes/routes.dart';
void main() {
FluroRouter.setupRouter();
// run app
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: FluroRouter.router.generator,
);
}
}
routes.dart
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
class FluroRouter {
static Router router = Router();
static Handler _storyhandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
HomeView(id: params['id'][0]));
static Handler _homehandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
Home());
static void setupRouter() {
router.define(
'/',
handler: _homehandler,
);
router.define(
'/story/:id',
handler: _storyhandler,
);
}
}
you can also define routes with query parameters.
Hope this helps!
you must use of Navigator v2 for Web.
see more info: here and here

How to mock navigation arguments for testing flutter screen widgets?

I would like to write a mockito test for a screen widget in flutter. The problem is, that this widget uses a variable from the navigation argument and I'm not sure how to mock this variable.
This is the example screen:
class TestScreen extends StatefulWidget {
static final routeName = Strings.contact;
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
Contact _contact;
#override
Widget build(BuildContext context) {
_contact = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(title: Text(Strings.contact)),
body: Text(_contact.name),
);
}
}
With this command I open the screen
Navigator.pushNamed(context, TestScreen.routeName, arguments: contact);
Normally I would mock some components, but I'm not sure how to mock the screen arguments. I hope it works something like this. However, I do not know what I can exactly mock.
when(screenArgument.fetchData(any))
.thenAnswer((_) async => expectedContact);
This is the current test, which of course is not working since _contact is null:
void main() {
testWidgets('contact fields should be filled with data from argument', (WidgetTester tester) async {
// GIVEN
final testScreen = TestApp(widget: TestScreen());
// WHEN
await tester.pumpWidget(testScreen);
// THEN
expect(find.text("test"), findsOneWidget);
});
}
An ugly way would be to use constructor parameters for the screen only for testing, but I want to avoid that.
Maybe someone of you knows how to best test such screen widgets.
The way that I've found is the same approach how flutter guys are testing it:
https://github.com/flutter/flutter/blob/d03aecab58f5f8b57a8cae4cf2fecba931f60673/packages/flutter/test/widgets/navigator_test.dart#L715
Basically they create a MaterialApp, put a button that after pressing will navigate to the tested page.
My modified solution:
Future<void> pumpArgumentWidget(
WidgetTester tester, {
#required Object args,
#required Widget child,
}) async {
final key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
home: FlatButton(
onPressed: () => key.currentState.push(
MaterialPageRoute<void>(
settings: RouteSettings(arguments: args),
builder: (_) => child,
),
),
child: const SizedBox(),
),
),
);
await tester.tap(find.byType(FlatButton));
await tester.pumpAndSettle(); // Might need to be removed when testing infinite animations
}
This approach works ok-ish, had some issues with testing progress indicators as it was not able to find those even when debugDumpApp displayed them.
If you are using a Dependency Injector such as I am, you may need to avoid pass contextual arguments to the constructor if your view is not built at the time the view class is instantiated. Otherwise, just use the view constructor as someone suggested.
So if you can't use constructor as I can't, you can solve this using Navigator directly in your tests. Navigator is a widget, so just use it to return your screen. Btw, it has no problem with Progress Indicator as pointed above.
import 'package:commons/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MyCustomArgumentsMock extends Mock implements MyCustomArguments {}
void main() {
testWidgets('indicator is shown when screen is opened', (tester) async {
final MyCustomArguments mock = MyCustomArgumentsMock();
await tester.pumpWidget(MaterialApp(
home: Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute<Widget>(
builder: (_) => TestScreen(),
settings: RouteSettings(arguments: mock),
);
},
),
));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}