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.