flutterblocflutter-go-routerflutter-dialog

Context: Navigator operation requested with a context that does not include a Navigator


I know there are several similar questions listed, but I can not resolve it after I tried some solutions from previous questions. :(

I want to do a network check and show a dialog if it has no network connection, so I did the following:

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          lazy: false,
          create: (context) => NetworkBloc()
            ..add(
              const StartMonitor(),
            ),
        ),
      ],
      child: MaterialApp.router(
        routerConfig: router,
        builder: (context, child) {
          return SubWidget(
            child: child,
          );
        },
      ),
    );
  }
}

//try to create a sub-widget
class SubWidget extends StatelessWidget {
  const SubWidget({super.key, this.child});

  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return UpgradeAlert(
      navigatorKey: router.routerDelegate.navigatorKey,
      child: Builder(//try to add build
          builder: (context) {
        return BlocListener<NetworkBloc, NetworkState>(
          listener: (context, state) async {
            if (state is NetworkFailure) {
              await showDialog(
                  context: context,
                  builder: (context) {
                    return const AlertDialog(
                      title: Text('No Network'),
                    );
                  });
            }
          },
          child: child,
        );
      }),
    );
  }
}

the network connection check is fine, but the problem is with showing the dialog. I know there might be some problems with the context, so I tried to create a sub-widget or use a Builder (), but the problem persists.


Solution

  • The parameter builder of MaterialApp works as a wrapper above the inner widgets or inhereted widgets it creates (MediaQuery, Navigator, etc) so in that context those widgets don't exist yet (they are below). Your widget tree is something like this:

    This is from the builder parameter

    {@macro flutter.widgets.widgetsApp.builder}
    
    Material specific features such as [showDialog] and [showMenu], and widgets   
    such as [Tooltip], [PopupMenuButton], also require a [Navigator] to properly function.
    

    MaterialApp (and WidgetsApp if you don't want any material or related theme) is a widget that helps build other important aspects of your app so you don't waste time doing it over and over again for each project, that's why you pass it your themes, routes and everything and then with that information it create all the inner widgets required to make the app functional (Theme, Tooltip, Navigator, ScaffoldMessenger, MediaQuery, etc).

    After its done it nests all those widgets and pass the navigator from your router so all your code can use Theme.of, Navigator.of, and all those static methods requiring context (a locator of Flutter so it can find and instance of that object above it). Without MaterialApp those methods will return null or an error (maybeOf and of respectively)

    What the builder parameter does in MaterialApp is letting you build a widget between your other classes (Theme, MediaQuery) and the navigator, why? because some people would use that to put some global information and don't depend on the navigator and avoid being removed when using a pop. The builder gives you 2 values (BuildContext and Widget) and expects a widget. That context has all the information mentioned above except the Navigator, that one is the 2nd parameter (that widget is the navigator itself) so it expects you wrap in some other widget or whatever and return it somehow so you have your Widget with some tweaks (colors, dialogs logic, listeners) and then your navigator


    What you need is to use your navigator key in your dialog context, and from there use navigatorKey.currentContext;

    class SubWidget extends StatelessWidget {
      const SubWidget({super.key, this.child, required this navigator});
    
      final Widget? child;
    
      @override
      Widget build(BuildContext context) {
        return UpgradeAlert(
          navigatorKey: router.routerDelegate.navigatorKey, /// the context of this key is the one required to make dialogs
          child: Builder(//try to add build
              builder: (context) {
            return BlocListener<NetworkBloc, NetworkState>(
              listener: (context, state) async {
                if (state is NetworkFailure) {
                  await showDialog(
                      context: router.routerDelegate.navigatorKey.currentContext!, /// It may be null but if you initialize it in your route there shouldn't be a problem
                      builder: (context) {
                        return const AlertDialog(
                          title: Text('No Network'),
                        );
                      });
                }
              },
              child: child,
            );
          }),
        );
      }
    }
    

    There is no need to make a Builder wrapper. That's the reason UpdateDialog from that package asks you to pass the navigator key, to do exactly the same you're trying and avoid depending on other contexts