flutterdartriverpod

An exception is thrown every time context.go is attempted inside the Riverpod data function


I am using go_router and Riverpod to achieve manageable state management and page routing. Unfortunately, I can't perform navigation inside the data function with two nested providers with StateNotifierProvider and StateNotifier setup. It is performed in a bottom sheet widget class. It works fine if I didn't perform context.go inside the data function but I require context.go.

This is my relevant source code in attempting navigation inside the data function:

someProvider.when(
  loading: () => Center(
    child: LoadingAnimationWidget.stretchedDots(
      color: Colors.white,
      size: 50,
    ),
  ),
  error: (error, stack) => RetainScale(
      child: Text("Error loading data")),
  data: (someDataModel) {
    final bool existed = (someDataModel.someListData ?? [])
            .where((find) => find.id == watchID)
            .firstOrNull
            ?.existed??
        false;
    return someAnotherProvider.when(
      loading: () => Center(
        child: LoadingAnimationWidget.stretchedDots(
          color: Colors.white,
          size: 50,
        ),
      ),
      error: (error, stack) => RetainScale(
          child: Text("Error loading data")),
      data: (someAnotherDataModel) {
        final qty = (someAnotherDataModel.someListData ?? [])
                .where((find) => find.id == watchID)
                .firstOrNull
                ?.qty ??
            0;
        return SizedBox(
          width: double.infinity,
          child: ElevatedButton(
            onPressed: () async {

              if (_selectedString == '') {
                if (context.mounted) {
                  _dialog.showAutoDismissDialog(
                      context,
                      'Please select something',
                      CupertinoIcons.exclamationmark_circle_fill,
                      Colors.redAccent);
                }
                return;
              }

              if ((qty == 0) || !existed) {
                ref
                    .read(dashboardBottomAppBarIndexProvider.notifier)
                    .setIndex(); // This is not the cause of an error, I've tested it out already

                Future(() {
                  if (context.mounted) {
                    kIsWeb
                        ? ref
                            .read(navigationProvider.notifier)
                            .navigateToPath(context, '/path-a')
                        : ref
                            .read(navigationProvider.notifier)
                            .navigateToPath(context, '/path-b');
                  }
                });

                return;
              }

              await _asyncProcess();
            },
            style: ButtonStyle(
              backgroundColor: watchID == ''
                  ? isDarkMode
                      ? WidgetStatePropertyAll<Color>(
                          Color.fromARGB(190, 255, 193, 7))
                      : WidgetStatePropertyAll<Color>(
                          Colors.lightBlue)
                  : ((qty == 0) || !existed)
                      ? WidgetStatePropertyAll<Color>(
                          Colors.redAccent)
                      : isDarkMode
                          ? WidgetStatePropertyAll<Color>(
                              Color.fromARGB(190, 255, 193, 7))
                          : WidgetStatePropertyAll<Color>(
                              Colors.lightBlue),
            ),
            child: RetainScale(
              child: Text(
                watchID == ''
                    ? _showText.toString()
                    : (qty == 0 || !existed)
                        ? 'Go somewhere'
                        : '$_showText QTY: $qty IF EXIST: $existed',
                style:
                    Theme.of(context).textTheme.bodyLarge?.copyWith(
                          fontWeight: FontWeight.bold,
                        ),
              ),
            ),
          ),
        );
      },
    );
  },
),

Even I replace this code snippet,

kIsWeb
    ? ref
        .read(navigationProvider.notifier)
        .navigateToPath(context, '/path-a')
    : ref
        .read(navigationProvider.notifier)
        .navigateToPath(context, '/path-b');

with this code snippet,

kIsWeb
    ? context.go('/path-a')
    : context.go('/path-b');

Still encountering an error:

StateNotifierListenerError (At least listener of the StateNotifier Instance of 'ButtonStateNotifier' threw an exception when the notifier tried to update its state.

The exceptions thrown are:

Tried to modify a provider while the widget tree was building. If you are encountering this error, chances are you tried to modify a provider in a widget life-cycle, such as but not limited to:

  • build
  • initState
  • dispose
  • didUpdateWidget
  • didChangeDependencies

Modifying a provider inside those life-cycles is not allowed, as it could lead to an inconsistent UI state. For example, two widgets could listen to the same provider, but incorrectly receive different states.

EDIT: Relevant code snippets of the StateNotifierProvider and StateNotifier

StateNotifier

class SomeDataModelNotifier
    extends StateNotifier<AsyncValue<SomeDataModel>> {
  SomeDataModelNotifier()
      : super(AsyncValue.data(SomeDataModel(someListField: [])));

  Future<void> initReqFunction(
      String endPoint, String id, String key) async {
    try {
      state = AsyncValue.loading();
      final response = await _dio.requestData(
          endPoint, id, "", "", "", 0, "", key);

      final List responseListData = response
          .map((json) => json as Map<String, dynamic>)
          .toList() as List<dynamic>;

      final List<SomeListField> jsonListData =
          responseListData
              .map((data) => SomeListField.fromJson(
                  data as Map<String, dynamic>))
              .toList();

      if (!mounted) {
        return;
      } // This important to resolve invalidate state processing exception

      state = AsyncValue.data(
          SomeDataModel(someListField: jsonListData));
    } catch (e, stackTrace) {
      if (!mounted) {
        return;
      } // This important to resolve invalidate state processing exception
      state = AsyncValue.error(e, stackTrace);
    }
  }
}

StateNotifierProvider

final someDataModelProvider = StateNotifierProvider<
    SomeDataModelNotifier, AsyncValue<SomeDataModel>>((ref) {
  return SomeDataModelNotifier();
});

Inside my bottom sheet widget class build function, I accessed the provider through this:

final AsyncValue<SomeDataModel> someProvider =
        ref.watch(someDataModelProvider);

What I'm trying to achieve is to not trigger an exception every time I perform context.go or navigation from my bottom sheet widget class.


Solution

  • I overlooked the ButtonStateNotifier class.

    The error is thrown because of this StateNotifier:

    class ButtonStateNotifier extends StateNotifier<bool> {
      ButtonStateNotifier() : super(true);
    
      // Method to set initial data from the parent class
      void isButtonEnabled({bool isEnabled = true}) {
        if (!mounted) {
          return;
        } // This important to resolve invalidate state processing exception
        state = isEnabled;
      }
    }
    

    and this is the StateNotifierProvider:

    final checkButtonStateProvider =
        StateNotifierProvider<ButtonStateNotifier, bool>((ref) {
      return ButtonStateNotifier();
    });
    

    My workaround findings suggest avoiding modifying any notifier that is currently in use by another widget through ref.watch or in any way where it expects to have a similar state while the widget tree is building.

    It is the same as what exactly says here:

    StateNotifierListenerError (At least listener of the StateNotifier Instance of 'ButtonStateNotifier' threw an exception when the notifier tried to update its state.

    The exceptions thrown are:

    Tried to modify a provider while the widget tree was building...

    To suppress the exception and proceed to perform context.go inside the data function.

    Remove or refactor the code where you perform ref.read for the ButtonStateNotifier which you intentionally or unintentionally modify its state.

    To address it properly, perform breakpoint to each instance of,

     ref.read(checkButtonStateProvider.notifier).isButtonEnabled();
    

    With this workaround, the error:

    StateNotifierListenerError (At least listener of the StateNotifier Instance of 'ButtonStateNotifier' threw an exception when the notifier tried to update its state.

    will not be triggered again...

    Important note: The ButtonStateNotifier is predefined by me. So it may vary depending on how you created your StateNotifier class.

    To be more specific, I deliberately placed the:

     ref.read(checkButtonStateProvider.notifier).isButtonEnabled();
    

    inside the PopScope class (for some feature-related reasons), since I have no further modification in the related features that I've been working with during the time I implemented that UX. So the error triggers when it attempts to go to another widget class through the use of context.go (while the bottomsheet is currently on the top of the widget tree, or currently showing to the client). I also found out that the most efficient solution is:

    PopScope<Object?>(
            canPop: false,
            onPopInvokedWithResult: (bool didPop, Object? result) {
              if (didPop) {
                return;
              }
              ref
                  .read(checkButtonStateProvider.notifier)
                  .isButtonEnabled();
              if (RouteTransitions.rootKey.currentState != null &&
                  RouteTransitions.rootKey.currentState!.canPop()) {
                RouteTransitions.rootKey.currentState?.pop();
              }
            },
            child: MyBottomsheetClass()
          ),
    

    Instead of removing this,

     ref.read(checkButtonStateProvider.notifier).isButtonEnabled();
    

    I placed it after the,

    if (didPop) {
      return;
    }
    

    The error is now suppressed.