'Null check operator error when widget testing using Flutter Modular's context.watch extension

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()),
      },
    );
  }
}


Solution 1:[1]

After reading through the test files in the Flutter Modular project, I discovered that they are wrapping everything inside a ModularApp when first pumping their widget tree.

For example, where I was using:

await tester.pumpWidget(MyWidgetUnderTest());

I should have been using:

await tester.pumpWidget(
  ModularApp(
    module: Module(),
    child: MyWidgetUnderTest(),
  ),
);

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 the module property (as opposed to a custom testing module or a mocked module), but I have asked the team for more information/documentation.

Solution 2:[2]

I am not familiar with the Modular package but I am pretty sure, it's because you didn't provide the Counter to your ConsumerClass. With packages like Provider, for example, you would wrap the parent of your ConsumerClass with a CounterProvider that would provide an instance of Counter to the consumer class, I assume there is something similar for the Flutter Modular Package.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Andrew H
Solution 2 Boris Kayi