那些初学者实践 Flutter 最常出现的错误

栏目: IT技术 · 发布时间: 4年前

内容简介:典型错误信息:这个错误常出现在异步任务处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。

哔哩哔哩漫画APP实践Flutter 也有近半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。

典型错误一:异步容错

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

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

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

示例代码

一段很常见的获取网络数据的代码,调用 requestApi ,获取 response ,进而 setState 刷新 Widget:

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

原因分析

response 的获取为异步任务( Future ),完全有可能在 AWidgetStatedispose 之后返回。故而在 setState 时需要容错。

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

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

这个 mounted 检查很重要,其实只要涉及到异步回调,都不要忘了检查该值。

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

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

又比如,在动画监听的回调里做点更新 Widget 的事:

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

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

典型错误二: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,错误出现。

另外,代码里的 Navigator.of(context) 所用的 context 其实也不是很正确,特别是当你的APP里有 Navigator 嵌套时,理论上应该用 builder 里传过来的 context

解决办法

首先,确保 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.currentContextnull 意为着该 dialog 已经被 dispose ,亦即从 WidgetTree 中 unmount

典型错误三: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;
  }
  //...
}

很明显, ScrollControlleroffest 是从 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 了。

  2. 为什么多了(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)
}

针对 No element 错误,确保 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) => ...;
}

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

典型问题五:泛型里的 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 语言 所导致的,关键是要学会容错处理。

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

最后,如果你有什么错误信息认为较为典型,欢迎 留言


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Real-Time Rendering, Third Edition

Real-Time Rendering, Third Edition

Tomas Akenine-Moller、Eric Haines、Naty Hoffman / A K Peters/CRC Press / 2008-7-25 / USD 102.95

Thoroughly revised, this third edition focuses on modern techniques used to generate synthetic three-dimensional images in a fraction of a second. With the advent or programmable shaders, a wide varie......一起来看看 《Real-Time Rendering, Third Edition》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试