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

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

内容简介:典型错误信息:这个错误常出现在异步任务处理,比如某个页面请求一个网络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 语言 所导致的,关键是要学会容错处理。

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

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


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

查看所有标签

猜你喜欢:

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

Mastering Regular Expressions, Second Edition

Mastering Regular Expressions, Second Edition

Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95

Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换