I'm using the Flutter Modular package for the first time and might be doing something wrong, but I'm getting a Null check operator used on a null value error any time I run a widget test with a widget that uses Flutter Modular's context.watch extension method.
The error always reads something like:
The following _CastError was thrown building DailyBalanceGraph(dirty, state:
_DailyBalanceGraphState#87329):
Null check operator used on a null value
The relevant error-causing widget was:
DailyBalanceGraph
DailyBalanceGraph:file:///Users/xxxx/Projects/xxxx/lib/modules/home/widgets/home_page_activity_display.dart:36:21
When the exception was thrown, this was the stack:
#0 _ModularInherited.of (package:flutter_modular/src/presenter/widgets/modular_app.dart:105:32)
#1 ModularWatchExtension.watch (package:flutter_modular/src/presenter/widgets/modular_app.dart:193:30)
#2 _DailyBalanceGraphState.build (package:cash4cast/modules/home/widgets/daily_balance_graph.dart:94:17)
#3 StatefulElement.build (package:flutter/src/widgets/framework.dart:4870:27)
#4 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4754:15)
#5 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4735:5)
#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4919:11)
#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4729:5)
... Normal element mounting (39 frames)
#48 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3790:14)
#49 Element.updateChild (package:flutter/src/widgets/framework.dart:3540:18)
#50 SliverMultiBoxAdaptorElement.updateChild (package:flutter/src/widgets/sliver.dart:1243:37)
#51 SliverMultiBoxAdaptorElement.createChild.<anonymous closure> (package:flutter/src/widgets/sliver.dart:1228:20)
#52 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2600:19)
#53 SliverMultiBoxAdaptorElement.createChild (package:flutter/src/widgets/sliver.dart:1221:12)
#54 RenderSliverMultiBoxAdaptor._createOrObtainChild.<anonymous closure> (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:349:23)
#55 RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:1997:59)
#56 PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:918:15)
#57 RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:1997:14)
#58 RenderSliverMultiBoxAdaptor._createOrObtainChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:338:5)
#59 RenderSliverMultiBoxAdaptor.insertAndLayoutChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:484:5)
#60 RenderSliverFixedExtentBoxAdaptor.performLayout (package:flutter/src/rendering/sliver_fixed_extent_list.dart:250:17)
#61 RenderObject.layout (package:flutter/src/rendering/object.dart:1887:7)
#62 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:137:12)
#63 _RenderSliverFractionalPadding.performLayout (package:flutter/src/widgets/sliver_fill.dart:167:11)
#64 RenderObject.layout (package:flutter/src/rendering/object.dart:1887:7)
#65 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:510:13)
#66 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1580:12)
#67 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1489:20)
#68 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1731:7)
#69 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:887:18)
#70 AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1131:23)
#71 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#72 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#73 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#74 AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:995:9)
#77 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#78 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:982:27)
#79 WidgetTester.pumpAndSettle.<anonymous closure> (package:flutter_test/src/widget_tester.dart:668:23)
<asynchronous suspension>
<asynchronous suspension>
(elided 3 frames from dart:async and package:stack_trace)
flutter --version output:
Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.gitFramework • revision 7e9793dee1 (3 weeks ago) • 2022-03-02 11:23:12 -0600Engine • revision bd539267b4Tools • Dart 2.16.1 • DevTools 2.9.2
Flutter Modular version ^4.4.0+1
A short example of the call that is failing would be something like:
class Counter extends ChangeNotifier {
int _count = 0;
int get count {
return _count;
}
void increment() {
_count++;
notifyListeners();
}
}
class ConsumerClass extends StatelessWidget {
const ConsumerClass();
@override
Widget build(BuildContext context) {
// when debugging the test, it will fail on this line; the context is not null, but
// it will fail inside the .watch call without ever entering the Counter class
final int count = context.watch<Counter>().count;
return Text(count.toString());
}
}
Then, in a test file:
import 'package:flutter_test/flutter_test.dart';
void main() {
setUp(() {
initModule(AppModule());
});
testWidgets('should instantiate', (tester) async => {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(
child: ConsumerClass(),
),
),
));
});
}
The only solution I have found is to completely remove all references to context.watch and instead wrap my widgets in an AnimatedBuilder, resulting in something like this:
class ConsumerClass extends StatelessWidget {
const ConsumerClass();
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Modular.get<Counter>(),
builder: (context, child) {
final int count = Modular.get<Counter>().count;
return Text(count.toString()),
},
);
}
}
After reading through the test files in the Flutter Modular project, I discovered that they are wrapping everything inside a
ModularAppwhen first pumping their widget tree.For example, where I was using:
I should have been using:
I implemented this in some of my tests and it seems to work as expected!
It also seems safe to use the default
Module()constructor as themoduleproperty (as opposed to a custom testing module or a mocked module), but I have asked the team for more information/documentation.