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.
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);
}
}
}
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.
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.