mobx.dart icon indicating copy to clipboard operation
mobx.dart copied to clipboard

Error when using fireImmediately with ReactionBuilder

Open ciprig opened this issue 1 year ago • 1 comments

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 avatar Dec 06 '24 15:12 ciprig

@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.

amondnet avatar May 23 '25 06:05 amondnet