内容简介:典型错误信息:这个错误常出现在异步任务处理,比如某个页面请求一个网络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
),完全有可能在 AWidgetState
被 dispose
之后返回。故而在 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.currentContext
为 null
意为着该 dialog 已经被 dispose
,亦即从 WidgetTree 中 unmount
。
典型错误三:ScrollController 里薛定谔的 position
在获取 ScrollController
的 position
、 offset
,或者调用 jumpTo()
等方法时,常出现 StateError
错误。
错误信息: StateError Bad state: Too many elements
, StateError 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); } }
-
为什么没有数据(No element):
ScrollController
还没有attach
一个position
。原因有两个:一个可能是还没被 mount 到树上(没有被Scrollable
使用到);另外一个就是已经被detach
了。 -
为什么多了(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 语言 所导致的,关键是要学会容错处理。
但容错办法又来自于一次次经验教训,谁也不能凭空就认识到要做什么样的错误处理,所以相信在经过一段时间到处踩坑的洗礼后,初学者也可以快速成长,将来各个都是精通。
最后,如果你有什么错误信息认为较为典型,欢迎 留言 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。