Flutter 最常出现的典型错误

4,517 阅读4分钟

错误一:无法掌握的Future

典型错误信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.

这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。

异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted,继续调用 setState 就会出现这个错误。

示例代码

  /// 话题详情
  void fetchDetailData({int articleId}) {
    TopicService.fetchTopicDetail(articleId: articleId).then((result) {
      if (result.code == 2000) {
        _topicDetailData = result.data;
      } else {
        showToast('获取获取详情失败: ${result.message}');
        Navigator.pop(context);
      }
    });
  }

原因分析

result 的获取为async-await异步任务,完全有可能在_TopicDetailPageState被 dispose之后才等到返回,那时候和该State 绑定的 Element 已经不在了。故而在setState时需要容错。

解决方法

setState之前检查是否 mounted
  /// 话题详情
  void fetchDetailData({int articleId}) {
    TopicService.fetchTopicDetail(articleId: articleId).then((result) {
      if (result.code == 2000) {
        _topicDetailData = result.data;
      } else {
        showToast('获取获取详情失败: ${result.message}');
        Navigator.pop(context);
      }
      if (mounted) setState(() {}); // 加了这行
    });
  }

这个mounted检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。 比如,在 FrameCallback里执行一个动画(AnimationController):

@override
void initState(){
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) _animationController.forward();
  });
}

WidgetsBinding.instance.addPostFrameCallback 通过addPostFrameCallback可以做一些安全的操作,在有些时候是很有用的,它会在当前Frame绘制完后进行回调,并只会回调一次,如果要再次监听需要再设置。 AnimationController有可能随着 State 一起 dispose了,但是FrameCallback仍然会被执行,进而导致异常。

又比如,在动画监听的回调里搞点事:

    @override
void initState(){
  _animationController.animation.addListener(_handleAnimationTick);
}


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

同样的在_handleAnimationTick被回调前,State 也有可能已经被dispose了。

如果你还不理解为什么,请仔细回味一下Event loop 还有复习一下 Dart 的线程模型。

错误二:Navigator.of(context) 是个 null

典型错误信息:NoSuchMethodError: The method 'pop' was called on null.

常在 showDialog 后处理 dialog 的 pop() 出现。

示例代码

在某个方法里获取网络数据,为了更好的提示用户,会先弹一个 loading 窗,之后再根据数据执行别的操作...

// 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();

原因分析:

出错的原因在于—— Android 原生的返回键:虽然代码指定了barrierDismissible: false,用户不可以点半透明区域关闭弹窗,但当用户点击返回键时,Flutter 引擎代码会调用 NavigationChannel.popRoute(),最终这个 loading dialog 甚至包括页面也被关掉,进而导致Navigator.of(context)返回的是null,因为该context已经被unmount,从一个已经凋零的树叶上是找不到它的根的,于是错误出现。

另外,代码里的Navigator.of(context) 所用的context也不是很正确,它其实是属于showDialog调用者的而非 dialog 所有,理论上应该用builder里传过来的context,沿着错误的树干虽然也能找到根,但实际上不是那么回事,特别是当你的APP里有Navigator嵌套时更应该注意。

解决办法:

首先,确保 Navigator.of(context)context 是 dialog 的context;其次,检查 null,以应对被手动关闭的情况。

showDialog 时传入 GlobalKey,通过 GlobalKey 去获取正确的context

GlobalKey key = GlobalKey();

showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (_) {
    return KeyedSubtree(
      key: key,
      child: Center(
        child: CircularIndicator(),
      )
    );
  },
);
var data = (await requestApi(...)).data;


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

key.currentContext 为null意为着该 dialog 已经被dispose,亦即已经从 WidgetTree 中unmount。

其实,类似的XXX.of(context)方法在 Flutter 代码里很常见,比如 MediaQuery.of(context)Theme.of(context)DefaultTextStyle.of(context)DefaultAssetBundle.of(context)等等,都要注意传入的context是来自正确节点的,否则会有惊喜在等你。

写 Flutter 代码时,脑海里一定要对context的树干脉络有清晰的认知,如果你还不是很理解context,可以看看 《深入理解BuildContext》 - Vadaski。

错误三:泛型里的 dynamic 一点也不 dynamic

典型错误信息:

* type 'List<dynamic>' is not a subtype of type 'List<int>'

* type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'

常发生在给某个List、Map 变量赋值时。 这种错误,也较常发生在使用服务端返回的数据model时。

示例代码

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

原因分析

jsonDecode() 这个方法转换出来的map的泛型是 Map<String, dynamic>,意为 value 可能是任何类型(dynamic),当 value 是容器类型时,它其实是List<dynamic>或者Map<dynamic, dynamic>等等。

而 Dart 的类型系统中,虽然dynamic可以代表所有类型,在赋值时,如果数据类型事实上匹配(运行时类型相等)是可以被自动转换,但泛型里 dynamic 是不可以自动转换的。可以认为 List<dynamic>List<int>是两种运行时类型。

解决办法:

使用 List.from, Map.from
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 {});
}

错误四:Android打包出现的问题1

原因分析

可能的原因是你安卓项目中的gradle版本和android plugin版本过高导致 位置: gradle: android/gradle/wrapper/gradle-wrapper.properties android plugin: android/build.gradle

解决办法:

  • 1.改成如图的版本就可以了
  • 2.或者不改版本运行以下命令
    1. $ flutter build --profile
    2. $ flutter build apk