Typical error one: Future that cannot be mastered

Typical error message: NoSuchMethodError: The method 'markNeedsBuild' was called on null.

This error is often found in asynchronous task (Future) processing, such as a page requesting a web API data and refreshing the Widget State based on the data.

This error occurs when the asynchronous task ends after the page has been popped, but does not check if the State is still mounted and continues to call setState.

Sample code

A very common code to get network data, call requestApi(), wait for Future to get response from it, and then setState to refresh the widget.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class AWidgetState extends State<AWidget> {
  // ...
  var data;
  void loadData() async {
    var response = await requestApi(...);
    setState((){
      this.data = response.data;
    })
  }
}

Cause analysis

The response is an async-await asynchronous task, and it is entirely possible that the AWidgetState is dispose before it returns, when the Element bound to the State is no longer there. So you need to tolerate errors when setState.

Solution: Check if mounted before setState.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class AWidgetState extends State {
  // ...
  var data;
  void loadData() async {
    var response = await requestApi(...);
    if (mounted) {
      setState((){
        this.data = response.data;
      })
    }
  }
}

Cause analysis

The response is an async-await asynchronous task, and it is entirely possible that the AWidgetState is dispose before it returns, when the Element bound to the State is no longer there. So you need to tolerate errors when setState.

Solution: Check if mounted before setState

1
2
3
4
5
6
@override
void initState(){
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) _animationController.forward();
  });
}

The AnimationController may dispose along with the State, but the FrameCallback will still be executed, leading to an exception.

Another example is to do something in the callback of the animation listener.

1
2
3
4
5
6
7
8
@override
void initState(){
  _animationController.animation.addListener(_handleAnimationTick);
}

void _handleAnimationTick() {
  if (mounted) updateWidget(...);
}

It is also possible that the State has been dispose before _handleAnimationTick is called back.

If you don’t understand why, please take a closer look at the Event loop and review Dart’s thread model.

Typical error #2: Navigator.of(context) is a null

Typical error message: NoSuchMethodError: The method 'pop' was called on null.

Often occurs in showDialog after processing pop() of dialog.

Sample code

Get network data in a method, to better prompt the user, a loading window will pop up first, and then perform other actions based on the data…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// show loading dialog on request data
showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (_) {
    return Center(
      child: CircularIndicator(),
    );
  },
);
var data = (await requestApi(...)).data;
// got it, pop dialog
Navigator.of(context).pop();

Cause Analysis.

The reason for the error is - Android’s native back button: although the code specifies barrierDismissible: false, the user cannot tap the translucent area to close the popup window, but when the user clicks the back button, the Flutter engine code will call NavigationChannel.popRoute() and eventually the loading dialog is turned off, including the page, which in turn causes Navigator.of(context) to return null because the context has been unmounted and cannot be found from an already withered The error occurs because the context has been unmounted and its root cannot be found from a faded leaf.

Also, the context used in the code Navigator.of(context) is not quite correct, it actually belongs to the showDialog caller and not to the dialog, in theory it should use the context passed from the builder, and the root can be found along the wrong trunk, but This is not the case in practice, especially if you have Navigator nested in your app.

Solution

First, make sure that the context of Navigator.of(context) is the context of the dialog. Second, you can wrap a WillPopScope around the Dialog Widget outer layer to handle the return key, or check null as a safety precaution in case it is manually closed.

Pass in GlobalKey when showDialog, and use GlobalKey to get the correct context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GlobalKey key = GlobalKey();

showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (_) {
     // Disallow pop by 'back' button when barrierDismissible is false
    return WillPopScope(
      onWillPop: () => Future.value(false),
      child: KeyedSubtree(
        key: key,
        child: Center(
          child: CircularIndicator(),
        )
      )
    );
  },
);
var data = (await requestApi(...)).data;

if (key.currentContext != null) {
  Navigator.of(key.currentContext)?.pop();
}

A key.currentContext of null means that the dialog has been dispose, i.e. unmounted from the WidgetTree.

In fact, similar XXX.of(context) methods are common in Flutter code, such as MediaQuery.of(context), Theme.of(context), DefaultTextStyle.of(context), DefaultAssetBundle.of(context) and so on, be careful that the context passed in is from the correct node, otherwise you will have a surprise waiting for you.

When writing Flutter code, make sure you have a clear idea of the trunk of context in your head.

Typical mistake #3: Schrödinger’s position in ScrollController

When getting the position or offset of the ScrollController or calling jumpTo() methods, StateError errors often occur.

Error messages: StateError Bad state: Too many elements, StateError Bad state: No element

Sample code

After a button click, the ScrollController controls the ListView scrolling to the beginning.

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
  if(_primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

Cause Analysis

First look at the source code of ScrollController.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ScrollController extends ChangeNotifier {
  //...
  @protected
  Iterable<ScrollPosition> get positions => _positions;
  final List<ScrollPosition> _positions = <ScrollPosition>[];
  
  double get offset => position.pixels;
  
  ScrollPosition get position {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
    return _positions.single;
  }
  //...
}

Obviously, the offest of the ScrollController is obtained from the position, which is derived from the variable _positions.

The StateError error is thrown by the _positions.single line.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
abstract class Iterable<E> {
  //...
  E get single {
    Iterator<E> it = iterator;
    if (!it.moveNext()) throw IterableElementError.noElement();
    E result = it.current;
    if (it.moveNext()) throw IterableElementError.tooMany();
    return result;
  }
//...
}

So the question is, why does this _positions suddenly not have a drop left, and suddenly it gives too much? ˊ_>?

We still have to go back to the source code of ScrollController to find out.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ScrollController extends ChangeNotifier {
  // ...
  void attach(ScrollPosition position) {
    assert(!_positions.contains(position));
    _positions.add(position);
    position.addListener(notifyListeners);
  }

  void detach(ScrollPosition position) {
    assert(_positions.contains(position));
    position.removeListener(notifyListeners);
    _positions.remove(position);
  }
}
  1. Why there is no data (No element). ScrollController has not attach a position yet. There are two reasons: one may be that it hasn’t been mounted to the tree (not used by Scrollable); the other is that it has been detach. 2.
  2. Why too many elements. ScrollController has attach a new one before it has time to detach the old position. The reason for this is most likely that the ScrollController is being used incorrectly and is being followed by multiple Scrollables at the same time.

Solution

For the No element error, just determine if _positions is empty, i.e. hasClients.

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
  if(_primaryScrollController.hasClients && _primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

For the too many elements error, make sure that the ScrollController is bound by only one Scrollable, don’t let it split, and is properly dispose().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class WidgetState extends State {
  final ScrollController _primaryScrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _primaryScrollController,
      itemCount: _itemCount,
      itemBuilder: _buildItem,
    )
  }

  int get _itemCount => ...;
  Widget _buildItem(context, index) => ...;

  @override
  void dispose() {
    super.dispose();
    _primaryScrollController.dispose();
  }
}

Typical mistake #4: Getting around null

The language Dart can be static or dynamic, and the type system is unique. Everything can be assigned the value null, which often causes comrades who are used to writing Java code to get dizzy because bool, int, double, which seem to be “primitive” types, are attached by null.

Typical error message.

  • Failed assertion: boolean expression must not be null
  • NoSuchMethodError: The method '>' was called on null.
  • NoSuchMethodError: The method '+' was called on null.
  • NoSuchMethodError: The method '*' was called on null.

Sample code

This error occurs more often when using data mods returned by the server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class StyleItem {
  final String name;
  final int id;
  final bool hasNew;

  StyleItem.fromJson(Map<String, dynamic> json):
    this.name = json['name'],
    this.id = json['id'],
    this.hasNew = json['has_new'];
}

StyleItem item = StyleItem.fromJson(jsonDecode(...));

Widget build(StyleItem item) {
  if (item.hasNew && item.id > 0) {
    return Text(item.name);
  }
  return SizedBox.shrink();
}

Cause analysis

StyleItem.fromJson() is not fault-tolerant to the data and should assume that the values in the map are likely to be null.

Solution: Error tolerance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class StyleItem {
  final String name;
  final int id;
  final bool hasNew;

  StyleItem.fromJson(Map<String, dynamic> json):
    this.name = json['name'],
    this.id = json['id'] ?? 0,
    this.hasNew = json['has_new'] ?? false;
}

Be sure to get used to Dart’s type system, anything can be null, for example, the following code, you can think of several possible errors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Test {
  double fraction(Rect boundsA, Rect boundsB) {
    double areaA = boundsA.width * boundsA.height;
    double areaB = boundsB.width * boundsB.height;
    return areaA / areaB;
  }
  
  void requestData(params, void onDone(data)) {
    _requestApi(params).then((response) => onDone(response.data));
  }
  
  Future<dynamic> _requestApi(params) => ...;
}

As a tip, onDone() can also be null.

Pay special attention when passing data with the native MethodChannel, and be careful.

Typical error 5: The dynamic in generic is not dynamic at all

Typical error message.

  • type 'List<dynamic>' is not a subtype of type 'List<int>'
  • type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'

Often occurs when assigning a value to a List, Map variable.

Sample code

This error also occurs more often when using the data mod returned by the server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Model {
  final List<int> ids;
  final Map<String, String> ext;

  Model.fromJson(Map<String, dynamic> json):
    this.ids = json['ids'],
    this.ext= json['ext'];
}

var json = jsonDecode("""{"ids": [1,2,3], "ext": {"key": "value"}}""");
Model m = Model.fromJson(json);

Reason analysis

The generic type of the map converted by jsonDecode() method is Map<String, dynamic>, meaning that value may be of any type (dynamic), and when value is a container type, it is actually List<dynamic> or Map<dynamic, dynamic>, etc.

In Dart’s type system, although dynamic can represent all types and can be automatically converted if the data types in fact match (runtime type is equal) when assigning values, the generic dynamic is not automatically convertible in generic types. Think of List<dynamic> and List<int> as two run-time types.

Solution: use List.from, Map.from

1
2
3
4
5
6
7
8
class Model {
  final List<int> ids;
  final Map<String, String> ext;

  Model.fromJson(Map<String, dynamic> json):
    this.ids = List.from(json['ids'] ?? const []),
    this.ext= Map.from(json['ext'] ?? const {});
}

Summary

To sum up, these typical errors are not troublesome, but caused by not understanding or not familiar with Flutter and Dart language, the key is to learn to handle fault tolerance.