内容简介:典型错误信息:这个错误常出现在异步任务处理,比如某个页面请求一个网络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 语言 所导致的,关键是要学会容错处理。
但容错办法又来自于一次次经验教训,谁也不能凭空就认识到要做什么样的错误处理,所以相信在经过一段时间到处踩坑的洗礼后,初学者也可以快速成长,将来各个都是精通。
最后,如果你有什么错误信息认为较为典型,欢迎 留言 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!