showDialog from root widget

Viewed 18997

I want to show a dialog from root widget (the one that created MaterialApp) I have a NavigatorState instance, but showDialog requires context that would return Navigator.of(context).

It looks like I need to provide context from a route, but I can't do this, because the root widget does not have it.

EDIT: I have found a workaround: I can push fake route that is only there to showDialog and then pop that route when dialog finishes. Not pretty but works.

7 Answers

I fixed the problem by using navigatorKey.currentState.overlay.context. Here is example:

class GlobalDialogApp extends StatefulWidget {
  @override
  _GlobalDialogAppState createState() => _GlobalDialogAppState();
}

class _GlobalDialogAppState extends State<GlobalDialogApp> {
  final navigatorKey = GlobalKey<NavigatorState>();

  void show() {
    final context = navigatorKey.currentState.overlay.context;
    final dialog = AlertDialog(
      content: Text('Test'),
    );
    showDialog(context: context, builder: (x) => dialog);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      home: Scaffold(
        body: Center(
          child: RaisedButton(
            child: Text('Show alert'),
            onPressed: show,
          ),
        ),
      ),
    );
  }
}

tl;dr: If you want to call showDialog from your root widget, extrude your code into another widget (e.g. a StatelessWidget), and call showDialog there.

Anyway, in the following I'm going to assume you are running into this issue:

flutter: No MaterialLocalizations found. 
flutter: MyApp widgets require MaterialLocalizations to be provided by a Localizations widget ancestor. 
flutter: Localizations are used to generate many different messages, labels,and abbreviations which are used by the material library.

As said before, showDialog can only be called in a BuildContext whose ancestor has a MaterialApp. Therefore you can't directly call showDialogif you have a structure like this:

- MaterialApp
  - Scaffold
    - Button // call show Dialog here

In a code example this would result in code like this, throwing the error given above:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: Scaffold(
        body: Center(
          child: RaisedButton(
              child: Text('Show dialog!'),
              onPressed: () {
                showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return Dialog(
                        child: Text('Dialog.'),
                      );
                    });
              }),
        ),
      ),
    );
  }
}

To solve this error from occuring you can create a new Widget, which has its own BuildContext. The modified structure would look like this:

- MaterialApp
  - Home

- Home     // your own (Stateless)Widget
  - Button // call show Dialog here

Modifying the code example to the structure given above, results in the code snippet below. showDialogcan be called without throwing the error.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: Home()
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
            child: Text('Show dialog!'),
            onPressed: () {
              showDialog(
                  context: context,
                  builder: (BuildContext context) {
                    return Dialog(
                      child: Text('Dialog.'),
                    );
                  });
            }),
      ),
    );
  }
}

They changed the way the navigator overlay works. This is the working solution for us as the accepted one isn't anymore.

// If you want to use the context for anything.
final context = navigatorKey.currentState.overlay.context;
// How to insert the dialog into the display queue.
navigatorKey.currentState.overlay.insert(anyDialog);

Since showDialog is used for showing a material dialog It can be used for showing dialogs inside a MaterialApp widget only. It can not be used to show dialog outside it.

If you already have a context object, you can get root material app's context by final rootContext = context.findRootAncestorStateOfType<NavigatorState>().context

and passing this to showDialog or showModalBottomSheet context argument.

If it helps anyone else, inject the navigator key into a dialog widget like so.

class MyApp extends StatelessWidget {
  MyApp({Key key});

  final navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      onGenerateRoute: Router.generateRoute,
      // ...
      builder: (context, routeChild) {
        return Material(
          child: InviteRequestModal(
            navigatorKey: navigatorKey,
            child: routeChild,
          ),
        );
      },
    );
  }

Then in the Widget that requires the modal, you can use it as mentioned above.

class InviteRequestModal extends StatelessWidget {
  final Widget child;
  final GlobalKey<NavigatorState> navigatorKey;

  InviteRequestModal({
    Key key,
    this.child,
    this.navigatorKey,
  }) : super(key: key);

  void _showInviteRequest(InviteRequest invite) {
    final context = navigatorKey.currentState.overlay.context;

    showDialog(
      context: context,
      builder: (_) {
        // Your dialog content
        return Container();
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener<InviteContactsBloc, InviteContactsState>(
      listenWhen: (previous, current) => current is InviteRequestLoaded,
      listener: (_, state) {
        if (state is InviteRequestLoaded) {
          _showInviteRequest(state.invite);
        }
      },
      child: child,
    );
  }
}

For those wanting to see how to do this in a multiple widget/route/file scenario, I used it with InheritedWidget and an extension on BuildContext.

main.dart

import 'package:flutter/material.dart';
import 'package:myapp/home_screen.dart';
import 'package:myapp/app_navkey.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final navigatorKey = GlobalKey<NavigatorState>();
  @override
  Widget build(BuildContext context) {
    return AppNavKey(
      navigatorKey: navigatorKey,
      child: MaterialApp(
        navigatorKey: navigatorKey,
        theme: ThemeData(),
        home: Scaffold(
          body: HomeScreen(),
        ),
      ),
    );
  }
}

home_screen.dart

import 'package:myapp/extensions.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final overlayContext = context.navigationKey().currentState.overlay.context;
    return Center(
      child: TextButton(
          child: Text('Show dialog!'),
          onPressed: () {
            showDialog(
                context: overlayContext, // use app level navigation context overlay
                builder: (BuildContext context) {
                  return Dialog(
                    child: Text('Dialog.'),
                  );
                });
          },
        ),
      );
  }
}

app_navkey.dart

import 'package:flutter/widgets.dart';

class AppNavKey extends InheritedWidget {
  final Widget child;
  final GlobalKey<NavigatorState> navigatorKey;

  AppNavKey({
    Key key,
    @required this.child,
    @required this.navigatorKey,
  }) : super(key: key, child: child);

  static GlobalKey<NavigatorState> of(BuildContext context) {
    final ctx = context.dependOnInheritedWidgetOfExactType<AppNavKey>();
    if (ctx == null) throw Exception('Could not find ancestor of type AppNavProvider');
    return ctx.navigatorKey;
  }

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}

extensions.dart

import 'package:flutter/widgets.dart';
import 'package:myapp/app_navkey.dart';

extension SwitchTabContext on BuildContext {
  /// Get app level NavigatorState key.
  /// ```dart
  /// context.navigationKey();
  /// ```
  GlobalKey<NavigatorState> navigationKey() => AppNavKey.of(this);
}
Related