zoukankan      html  css  js  c++  java
  • Flutter 掉坑错误

    典型错误一:无法掌握的Future

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

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

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

    示例代码

    一段很常见的获取网络数据的代码,调用 requestApi(),等待Future从中获取response,进而setState刷新 Widget:

    class AWidgetState extends State<AWidget> {
    // ...
    var data;
    void loadData() async {
    var response = await requestApi(...);
    setState((){
    this.data = response.data;
    })
    }
    }

    原因分析

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

    解决办法: setState之前检查是否 mounted

    class AWidgetState extends State {
    // ...
    var data;
    void loadData() async {
    var response = await requestApi(...);
    if (mounted) {
    setState((){
    this.data = response.data;
    })
    }
    }
    }

    这个mounted检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。

    比如,在 FrameCallback里执行一个动画(AnimationController):

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

    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。

    典型错误三:ScrollController 里薛定谔的 position

    在获取ScrollControllerpositionoffset,或者调用jumpTo()等方法时,常出现StateError错误。

    错误信息:StateError Bad state: Too many elementsStateError Bad state: No element

    示例代码

    在某个按钮点击后,通过ScrollController 控制ListView滚动到开头:

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

    原因分析

    先看ScrollController的源码:

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

    很明显,ScrollController 的 offest 是从 position 中获得,而position 则是来自变量 _positions

    StateError错误,就是_positions.single 这一行抛出:

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

    那么问题来了,这个_positions 为什么忽而一滴不剩,忽而却嫌它给的太多了呢?ˊ_>ˋ

    还是要回到 ScrollController 的源码里找找。

    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. 为什么没有数据(No element): ScrollController还没有 attach 一个 position。原因有两个:一个可能是还没被 mount 到树上(没有被Scrollable使用到);另外一个就是已经被 detach了。

    1. 为什么多了(Too many elements): ScrollController还没来得及 detach旧的 position,就又attach了一个新的。原因多半是因为ScrollController的用法不对,同一时间被多个 Scrollable关注到了。

    解决办法

    针对 No element 错误,只需判断一下 _positions是不是空的就行了,即hasClients

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

    针对 Too many elements 错误,确保ScrollController只会被一个 Scrollable绑定,别让它劈腿了,且被正确 dispose()

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

    典型错误四:四处碰壁 null

    Dart 这个语言可静可动,类型系统也独树一帜。万物都可以赋值null,就导致写惯了 Java 代码的同志们常常因为bool int double这种看起来是"primitive"的类型被null附体而头晕。

    典型错误信息:

    • 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.

    示例代码

    这种错误,较常发生在使用服务端返回的数据model时。

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

    原因分析

    StyleItem.fromJson() 对数据没有容错处理,应当认为 map 里的value都有可能是 null

    解决办法:容错

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

    一定要习惯 Dart 的类型系统,什么都有可能是null,比如下面一段代码,你细品有几处可能报错:

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

    小提示,onDone()也可以是null >﹏<。

    在和原生用 MethodChannel传数据时更要特别注意,小心驶得万年船。

    典型错误五:泛型里的 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 {});
    }

    总结

    综上所述,这些典型错误,都不是什么疑难杂症,而是不理解或者不熟悉 Flutter 和 Dart 语言所导致的,关键是要学会容错处理。

    但容错办法又来自于一次次经验教训,谁也不能凭空就认识到要做什么样的错误处理,所以相信在经过一段时间到处踩坑的洗礼后,初学者也可以快速成长,将来各个都是精通。


    学习来源:https://yrom.net/blog/2020/03/13/The-most-often-errors-in-Flutter/


     

  • 相关阅读:
    数据结构化与保存
    使用正则表达式,取得点击次数,函数抽离
    爬取校园新闻首页的新闻
    网络爬虫基础练习
    综合练习:词频统计
    Hadoop综合大作业
    理解MapReduce
    熟悉常用的Hbase操作
    熟悉的HDFS操作
    爬取晨星网对各公司主要职位的分析
  • 原文地址:https://www.cnblogs.com/loaderman/p/12501807.html
Copyright © 2011-2022 走看看