Flutter 开发中的一些 Tips(二)
1. setState() called after dispose()
这个是我偶然在控制台发现的,完整的错误信息如下:
Unhandled Exception: setState() called after dispose(): _AboutState#9c33a(lifecycle state: defunct, not mounted)
当然 flutter 在错误信息之后还有给出问题原因及解决方法:
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree. This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
大致的意思是,widget 已经在dispose
方法时销毁了,但在这之后却调用了setState
方法,那么会发生此错误。比如定时器或动画回调调用setState()
,但此时页面已关闭时,就会发生此错误。这个错误一般并不会程序崩溃,只是会造成内存的泄露。
那么解决的办法分为两部分:
及时停止或者销毁监听,例如一个定时器:
Timer _countdownTimer;
@overridevoid dispose() {_countdownTimer?.cancel();_countdownTimer = null;super.dispose();}
为了保险我们还要在调用
setState()
前判断当前页面是否存在:
_countdownTimer = Timer.periodic(Duration(seconds: 2), (timer) {if (mounted){setState(() {
});}});
我们可以看看 mounted
在源码中是什么
BuildContext get context => _element;StatefulElement _element;
/// Whether this [State] object is currently in a tree.////// After creating a [State] object and before calling [initState], the/// framework "mounts" the [State] object by associating it with a/// [BuildContext]. The [State] object remains mounted until the framework/// calls [dispose], after which time the framework will never ask the [State]/// object to [build] again.////// It is an error to call [setState] unless [mounted] is true.bool get mounted => _element != null;
BuildContext
是Element
的抽象类,你可以认为mounted
就是 context
是否存在。那么同样在回调中用到 context
时,也需要判断一下mounted
。比如我们要弹出一个 Dialog
时,或者在请求接口成功时退出当前页面。BuildContext
的概念是比较重要,需要掌握它,错误使用一般虽不会崩溃,但是会使得代码无效。
本问题详细的代码见:点击查看
2.监听 Dialog 的关闭
问题描述:我在每次的接口请求前都会弹出一个Dialog
做 loading 提示,当接口请求成功或者失败时关闭它。可是如果在请求中,我们点击了返回键人为的关闭了它,那么当真正请求成功或者失败关闭它时,由于我们调用了Navigator.pop(context)
导致我们错误的关闭了当前页面。
那么解决问题的突破口就是知道何时 Dialog 的关闭,那么就可以使用 WillPopScope
拦截到返回键的输入,同时记录到 Dialog 的关闭。
bool _isShowDialog = false;
void closeDialog() {if (mounted && _isShowDialog){_isShowDialog = false;Navigator.pop(context);}}
void showDialog() {/// 避免重复弹出 if (mounted && !_isShowDialog){isShowDialog = true;showDialog(context: context,barrierDismissible: false,builder:() {return WillPopScope(onWillPop: () async {// 拦截到返回键,证明 dialog 被手动关闭_isShowDialog = false;return Future.value(true);},child: ProgressDialog(hintText: "正在加载..."),);});}}
本问题详细的代码见:点击查看
3.addPostFrameCallback
addPostFrameCallback
回调方法在 Widget 渲染完成时触发,所以一般我们在获取页面中的 Widget 大小、位置时使用到。
前面第二点我有说到我会在接口请求前弹出 loading。如果我将请求方法放在了initState
方法中,异常如下:
inheritFromWidgetOfExactType(_InheritedTheme) or inheritFromElement() was called before initState() completed. When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget. Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
原因:弹出一个 DIalog 的showDialog
方法会调用Theme.of(context, shadowThemeOnly: true)
,而这个方法会通过inheritFromWidgetOfExactType
来跨组件获取 Theme 对象。
inheritFromWidgetOfExactType
方法调用inheritFromElement
:
但是在_StateLifecycle
为created
和 defunct
时是无法跨组件拿到数据的,也就是initState()
时和dispose()
后。所以错误信息提示我们在 didChangeDependencies
调用。
然而放在didChangeDependencies
后,新的异常:
setState() or markNeedsBuild() called during build. This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
提示我们页面在 build 时不能调用setState()
或 markNeedsBuild()
方法。所以我们需要在 build 完成后,才可以去创建这个新的组件(这里就是 Dialog)。
所以解决方法就是使用addPostFrameCallback
回调方法,等待页面 build 完成后在请求数据:
@overridevoid initState() {WidgetsBinding.instance.addPostFrameCallback((_){/// 接口请求});}
导致这类问题的场景很多,但是大体解决思路就是上述的办法。
本问题详细的代码见:点击查看
4.删除 emoji
不多哔哔,直接看图:
简单说就是删除一个 emoji 表情,一般需要点击删除两次。碰到个别的 emoji,需要删除 11 次!!其实这问题,也别吐槽 Flutter,基本 emoji 在各个平台上都或多或少有点问题。
原因就是:
这个问题我发现在 Flutter 的1.5.4+hotfix.2
版本,解决方法可以参考:github.com/flutter/eng… 虽然只适用于长度为 2 位的 emoji。
幸运的是在最新的稳定版1.7.8+hotfix.3
中修复了这个问题。不幸的是我发现了其他的问题,比如在我小米 MIX 2s 上删除文字时,有时会程序崩溃,其他一些机型正常。异常如下图:
我也在 Flutter 上发现了同样的问题 Issue,具体情况可以关注这个 Issue :github.com/flutter/flu… ,据 Flutter 团队的人员的回复,这个问题修复后不太可能进入 1.7 的稳定版本。。
所以建议大家谨慎升级,尤其是用于生产环境。那么这个问题暂时只能搁置下来了,等待更稳定的版本。。。
19.07.20 更新,官方发布了1.7.8+hotfix.4
,修复了此问题。经过测试问题修复,大家可以放心使用了。
5.键盘
1.是否弹起
MediaQuery.of(context).viewInsets.bottom > 0
viewInsets.bottom
就是键盘的顶部距离底部的高度,也就是弹起的键盘高度。如果你想实时过去键盘的弹出状态,配合使用didChangeMetrics
。完整如下:
import 'package:flutter/material.dart';
typedef KeyboardShowCallback = void Function(bool isKeyboardShowing);
class KeyboardDetector extends StatefulWidget {
KeyboardShowCallback keyboardShowCallback;
Widget content;
KeyboardDetector({this.keyboardShowCallback, @required this.content});
@override_KeyboardDetectorState createState() => _KeyboardDetectorState();}
class _KeyboardDetectorState extends State<KeyboardDetector>with WidgetsBindingObserver {@overridevoid initState() {WidgetsBinding.instance.addObserver(this);super.initState();}
@overridevoid didChangeMetrics() {super.didChangeMetrics();WidgetsBinding.instance.addPostFrameCallback((_) {print(MediaQ
uery.of(context).viewInsets.bottom);setState(() {widget.keyboardShowCallback?.call(MediaQuery.of(context).viewInsets.bottom > 0);});});}
@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);super.dispose();}
@overrideWidget build(BuildContext context) {return widget.content;
评论