sinaptia waves

Lesson learned: sharing context with Flutter dialogs

Nazareno Moresco
Dec 13th, 2023

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.