Lesson learned: sharing context with Flutter dialogs

In Flutter, dialogs don’t automatically share the context with the parent widget. We found out while implementing providers. Learn how to solve the problem.

Dec 13th, 2023
By Nazareno Moresco

tl;dr: To handle a ProviderNotFoundException in a Dialog, re-inject the ChangeNotifier into the dialog’s context using ChangeNotifierProvider.value.

Sometimes, the bugs we face are a window to learn about the inner workings of the tools we use, and one bug we recently faced is the one around accessing a ChangeNotifier in a Dialog.

So, what exactly is a ChangeNotifier? It’s a key component of the provider package in Flutter, used for state management. Essentially, a ChangeNotifier is an object that can be listened to, operating in a fashion similar to the Observer pattern.

When trying to access a ChangeNotifier inside a Dialog it typically leads to a ProviderNotFoundException. To understand this issue and learn how to resolve it, let’s dive into a practical example.

Imagine a simple ChangeNotifier, a RocketLauncher, that triggers a rocket launch which can either fail or succeed:

class RocketLauncher extends ChangeNotifier {
  int failed = 0;
  int successful = 0;

  int get count => failed + successful;

  void launch() {
    if (Random().nextBool()) {
      successful += 1;
    } else {
      failed += 1;
    }
    notifyListeners();
  }
}

This ChangeNotifier informs its listeners each time a launch occurs.

Our app structure looks like this:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider(
        create: (context) => RocketLauncher(),
        builder: (context, _) => const Scaffold(
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              LaunchButton(),
              StatsButton(),
            ],
          ),
        ),
      ),
    );
  }
}

Here, LaunchButton initiates launches, and StatsButton displays statistics. ChangeNotifierProvider makes the RocketLauncher available to its descendant widgets.

class LaunchButton extends StatelessWidget {
  const LaunchButton({super.key});

  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: context.watch<RocketLauncher>().launch,
      icon: const Icon(Icons.rocket_launch),
    );
  }
}

LaunchButton simply triggers the RocketLauncher’s launch method.

class StatsButton extends StatelessWidget {
  const StatsButton({super.key});

  @override
  Widget build(BuildContext context) {
    final launcher = context.watch<RocketLauncher>();

    return TextButton(
      child: Text("${launcher.count} launches"),
      onPressed: () {
        showDialog(
          context: context,
          builder: (context) => const StatsDialog(),
        );
      },
    );
  }
}

StatsButton shows the total launches and opens a dialog for more details.

class StatsDialog extends StatelessWidget {
  const StatsDialog({super.key});

  @override
  Widget build(BuildContext context) {
    final launcher = context.read<RocketLauncher>();

    return Dialog(
      child: Column(
        children: [
          Text("${launcher.failed} failed rocket launches"),
          Text("${launcher.successful} successful rocket launches"),
        ],
      ),
    );
  }
}

StatsDialog reads RocketLauncher to display the count of failed and successful launches.

However, opening the dialog results in this exception:

The following ProviderNotFoundException was thrown building StatsDialog(dirty):
Error: Could not find the correct Provider<RocketLauncher> above this StatsDialog Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice.

The reason why this error occurs is because showDialog in Flutter uses a different context thant its caller’s context. Flutter’s showDialog` documentation states: “The widget returned by the builder does not share a context with the location that showDialog is originally called from.”

To resolve this, we can pass the existing RocketLauncher instance to the dialog’s context using ChangeNotifierProvider.value:

class StatsButton extends StatelessWidget {
  const StatsButton({super.key});

  @override
  Widget build(BuildContext context) {
    final launcher = context.watch<RocketLauncher>();

    return TextButton(
      child: Text("${launcher.count} launches"),
      onPressed: () {
        showDialog(
          context: context,
          builder: (context) => ChangeNotifierProvider.value(
            value: launcher,
            builder: (context, _) => const StatsDialog(),
          ),
        );
      },
    );
  }
}

In conclusion, dealing with ProviderNotFoundException in Flutter is a common challenge that underscores the importance of understanding context management in Flutter’s framework. By using ChangeNotifierProvider.value, developers can efficiently bridge the context gap between the parent widgets and their dialogs.