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

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

内容简介:公众号回复哔哩哔哩漫画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 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;
}
//...
}

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);
}
}

  1. 为什么没有数据(No element): ScrollController 还没有  attach 一个  position 。原因有两个:一个可能是还没被  mount 到树上(没有被 Scrollable 使用到);另外一个就是已经被  detach 了。

  1. 为什么多了(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 语言所导致的,关键是要学会容错处理。

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

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

Flutter 是移动应用程序开发的未来?

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

如果你有写博客的好习惯

欢迎投稿

点个在看,小生感恩 :heart:


以上所述就是小编给大家介绍的《那些初学者实践 Flutter 最常出现的错误》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

ES6 标准入门(第2版)

ES6 标准入门(第2版)

阮一峰 / 电子工业出版社 / 2016-1 / 69.00元

ES6(又名 ES2105)是 JavaScript 语言的新标准,2015 年 6 月正式发布后,得到了迅速推广,是目前业界超级活跃的计算机语言。《ES6标准入门(第2版)》是国内仅有的一本 ES6 教程,在前版基础上增补了大量内容——对标准进行了彻底的解读,所有新增的语法知识(包括即将发布的 ES7)都给予了详细介绍,并且紧扣业界开发实践,给出了大量简洁易懂、可以即学即用的示例代码。 《......一起来看看 《ES6 标准入门(第2版)》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

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

多种字符组合密码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具