Error when using fireImmediately with ReactionBuilder
If I use ReactionBuilder with a fireImmediately reaction or autorun flutter throws the following error:
Error: dependOnInheritedWidgetOfExactType<_ScaffoldMessengerScope>() or dependOnInheritedElement() was called before ReactionBuilderState.initState() completed. When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget. Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
This is because the inherited widget is accessed using the context from ReactionBuilder before the initState of ReactionBuilder has finished.
This can be reproduced in the example app by changing connectivity_widgets.dart the reaction to fireImmediately
child: ReactionBuilder(
builder: (context) {
return reaction((_) => store.connectivityStream.value, (result) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')));
}, fireImmediately: true, delay: 4000);
},
@ciprig
This is a common Flutter lifecycle issue when using MobX's ReactionBuilder with fireImmediately: true. The problem occurs because the reaction fires during the widget's initialization phase, before initState() completes, but tries to access inherited widgets like ScaffoldMessenger which aren't fully available yet.
Here are several solutions to fix this:
Solution 1: Use WidgetsBinding.instance.addPostFrameCallback
Delay the inherited widget access until after the widget tree is fully built:
child: ReactionBuilder(
builder: (context) {
return reaction((_) => store.connectivityStream.value, (result) {
// Delay the ScaffoldMessenger access until after the frame is built
WidgetsBinding.instance.addPostFrameCallback((_) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')
));
});
}, fireImmediately: true, delay: 4000);
},
)
Solution 2: Use a StatefulWidget with proper lifecycle management
Instead of ReactionBuilder, create a custom widget that properly handles the lifecycle:
class ConnectivityReactionWidget extends StatefulWidget {
final Widget child;
const ConnectivityReactionWidget({Key? key, required this.child}) : super(key: key);
@override
State<ConnectivityReactionWidget> createState() => _ConnectivityReactionWidgetState();
}
class _ConnectivityReactionWidgetState extends State<ConnectivityReactionWidget> {
late ReactionDisposer _disposer;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Set up the reaction after dependencies are available
_disposer = reaction(
(_) => store.connectivityStream.value,
(result) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')
));
},
fireImmediately: true,
delay: 4000,
);
}
@override
void dispose() {
_disposer();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
Solution 3: Check if the widget is mounted and context is valid
Add safety checks before accessing inherited widgets:
child: ReactionBuilder(
builder: (context) {
return reaction((_) => store.connectivityStream.value, (result) {
// Check if the context is still valid and mounted
if (context.mounted) {
try {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')
));
} catch (e) {
// Handle the case where ScaffoldMessenger is not available
print('ScaffoldMessenger not available: $e');
}
}
}, fireImmediately: true, delay: 4000);
},
)
Solution 4: Don't use fireImmediately if not necessary
If you don't need the reaction to fire immediately, simply remove the fireImmediately: true parameter:
child: ReactionBuilder(
builder: (context) {
return reaction((_) => store.connectivityStream.value, (result) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')
));
}, delay: 4000); // Remove fireImmediately: true
},
)
Recommended Approach
I'd recommend Solution 1 (using addPostFrameCallback) as it's the simplest fix that maintains your desired behavior while ensuring the widget tree is fully initialized before accessing inherited widgets. This approach is commonly used in Flutter to defer operations that need to access the widget tree until after the current frame is complete.
The key insight is that fireImmediately: true causes the reaction to execute during widget initialization, but inherited widgets like ScaffoldMessenger need the full widget tree to be established first.