内容简介:公众号回复哔哩哔哩漫画APP实践Flutter 也有大半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。
code小生 一个专注大前端领域的技术平台
公众号回复 Android
加入安卓技术交流群
哔哩哔哩漫画APP实践Flutter 也有大半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。
典型错误一:无法掌握的Future
典型错误信息: NoSuchMethodError: The method 'markNeedsBuild' was called on null.
这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。
异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted
,继续调用 setState
就会出现这个错误。
示例代码
requestApi() response setState
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 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 builder context Navigator
解决办法
Navigator.of(context) context 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 dispose unmount
XXX.of(context) MediaQuery.of(context) Theme.of(context) DefaultTextStyle.of(context) DefaultAssetBundle.of(context) context
写 Flutter 代码时,脑海里一定要对 context
的树干脉络有清晰的认知,如果你还不是很理解 context
,可以看看 《深入理解BuildContext》 - Vadaski。
典型错误三: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)
}
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
null bool int double 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<String, dynamic> 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 语言所导致的,关键是要学会容错处理。
但容错办法又来自于一次次经验教训,谁也不能凭空就认识到要做什么样的错误处理,所以相信在经过一段时间到处踩坑的洗礼后,初学者也可以快速成长,将来各个都是精通。
最后,如果你有什么错误信息认为较为典型,欢迎 留言 。
如果你有写博客的好习惯
点个在看,小生感恩 :heart:
以上所述就是小编给大家介绍的《那些初学者实践 Flutter 最常出现的错误》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
年入10万,17岁草根少年的网赚实战
陶秋丰 / 重庆出版集团 / 2009-3 / 28.00元
《年入10万:17岁草根少年的网赚实战》以一个17岁的在校大学生的真实故事为大家讲述草根少年的网络赚钱之旅。随着网络的普及以及网上应用的日益增多,要在网络上谋生并不难,比如网上写稿、网上兼职、威客赚钱、网上开店等,然而要利用互联网赚大钱,并成就一番事业,那么创建并运营一个独立的网站就是一个绝佳的选择。本书的作者正是经历了“网上写稿一网上各类兼职一策划并创建网站一网站推广与运营一年入10万”这一过程......一起来看看 《年入10万,17岁草根少年的网赚实战》 这本书的介绍吧!