Flutter开发中的一些Tips(二)

接着上篇 Flutter开发中的一些Tips,今天再分享一些我遇到的问题,这篇较上一篇,细节方面更多,希望“引以为戒”,毕竟细节决定成败。本篇的所有例子,都在我开源的flutter_deer中。希望Star、Fork支持,有问题可以Issue。附上链接:https://github.com/simplezhli/flutter_deer

在这里插入图片描述

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(),但此时页面已关闭时,就会发生此错误。这个错误一般并不会程序崩溃,只是会造成内存的泄露。

那么解决的办法分为两部分:

  1. 及时停止或者销毁监听,例如一个定时器:
  Timer _countdownTimer;

  @override
  void dispose() {
    _countdownTimer?.cancel();
    _countdownTimer = null;
    super.dispose();
  }
  1. 为了保险我们还要在调用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;

BuildContextElement的抽象类,你可以认为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
在这里插入图片描述

但是在_StateLifecyclecreateddefunct 时是无法跨组件拿到数据的,也就是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完成后在请求数据:

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((_){
      /// 接口请求
    });
  }
 

导致这类问题的场景很多,但是大体解决思路就是上述的办法。

本问题详细的代码见:点击查看

4.删除emoji

不多哔哔,直接看图:

简单说就是删除一个emoji表情,一般需要点击删除两次。碰到个别的emoji,需要删除11次!!其实这问题,也别吐槽Flutter,基本emoji在各个平台上都或多或少有点问题。

原因就是:
在这里插入图片描述
这个问题我发现在Flutter 的1.5.4+hotfix.2版本,解决方法可以参考:https://github.com/flutter/engine/pull/7555 虽然只适用于长度为2位的emoji。

幸运的是在最新的稳定版1.7.8+hotfix.3中修复了这个问题。不幸的是我发现了其他的问题,比如在我小米MIX 2s上删除文字时,有时会程序崩溃,其他一些机型正常。异常如下图:

我也在Flutter上发现了同样的问题Issue,具体情况可以关注这个Issue :https://github.com/flutter/flutter/issues/33642 ,据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 {
  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      print(MediaQuery.of(context).viewInsets.bottom);
      setState(() {
        widget.keyboardShowCallback
            ?.call(MediaQuery.of(context).viewInsets.bottom > 0);
      });
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.content;
  }
}

代码来自项目GSYFlutterDemo:https://github.com/CarGuo/GSYFlutterDemo

2.弹出键盘

if (MediaQuery.of(context).viewInsets.bottom == 0){
  final focusScope = FocusScope.of(context);
  focusScope.requestFocus(FocusNode());
  Future.delayed(Duration.zero, () => focusScope.requestFocus(_focusNode));
}

其中_focusNode是对应的TextFieldfocusNode属性。

3.关闭键盘

FocusScope.of(context).requestFocus(FocusNode());
/// 1.7.8 开始推荐以下方式(https://github.com/flutter/flutter/issues/7247)
FocusScope.of(context).unfocus();
/// 2020.05.14补充:更更推荐下面的方式,避免调用上面的方法造成不必要页面的rebuild。
FocusManager.instance.primaryFocus?.unfocus();

这里提一下关闭,一般来说即使键盘弹出,点击返回页面关闭,键盘就会自动收起。但是顺序是:

页面关闭 --> 键盘关闭

这样会导致键盘短暂的出现在你的上一页面,也就会出现短暂的部件溢出(关于溢出可见上篇)。

所以这时你就需要在页面关闭前手动调用关闭键盘的代码了。按道理是要放到deactivate或者dispose中处理的,可谁让context已经为null了,所以,老办法,拦截返回键:

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // 拦截返回键
        FocusManager.instance.primaryFocus?.unfocus();
        return Future.value(true);
      },
      child: Container()
    );
  }   

当然如果你是自己代码调用的返回,可以在返回页面之前关闭键盘:

	FocusManager.instance.primaryFocus?.unfocus();
    Navigator.pop(context);

本问题详细的代码见:点击查看

6.Android 9.0适配

话说现在新建的Flutter项目,Android的 targetSdkVersion 默认都是28。所以不可避免的就是Android 9.0的适配甚至6,7,8的适配,那我碰到的一个问题是接入的高德2D地图在9.0的机子上显示不出来。

问题的主要原因是Android 9.0 要求默认使用加密连接,简单地说就是不允许使用http请求,要求使用https。高德的2D地图sdk怀疑是使用了http请求,所以会加载不出。

解决方法两个:

  1. targetSdkVersion 改为28以下(长远看来不推荐)

  2. android -> app - > src -> main -> res 目录下新建xml,添加network_security_config.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

AndroidManifest.xml中的application添加:

android:networkSecurityConfig="@xml/network_security_config"

这个问题只是Android适配中的一小部分,相应的iOS中也有适配问题。比如常用的权限适配等。

不得不说做Flutter的开发需要对原生开发有一定了解。尤其是之前在写Flutter的地图插件时感受深刻,那么我本来就是做Android开发的,所以Android端的部分很快就完成了。iOS部分就很吃力,首先OC的语法就不会,其次说实话写完了心里也没底,还是需要向iOS的同事请教确保一下。所以跨平台方案的出现并不会对原生开发造成冲击,反而是对原生开发提出了更高的要求。

本问题详细的代码见:点击查看

7.其他

  1. Flutter开发中的json解析确实很麻烦,当然有许多的插件来解决我们的问题。我个人推荐使用FlutterJsonBeanFactory。关于它的一系列使用可以参看:https://www.jianshu.com/nb/33360539

  2. UI层面的功能最好还是使用Flutter来解决。比如Toast功能,很多人都会选择fluttertoast这个插件,而我推荐oktoast这类使用Flutter的解决方案 。因为fluttertoast是调用了Android原生的Toast,首先在各个系统上的样式就不统一,同时部分系统机型上受限通知权限,会导致Toast无法弹出。

篇幅有限,那么先分享以上几条Tips,如果本篇对你有所帮助,可以点赞支持!其实收藏起来不是以后遇到问题时查找更方便吗?。

最后再次奉上Github地址:https://github.com/simplezhli/flutter_deer

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Flutter 开发,"DragScrollSheet" 是一个用于实现可拖动滚动的小部件。它允许用户在屏幕上拖动内容并进行滚动,类似于滚动视图(ScrollView),但具有更灵活的交互方式。 你可以使用 `DraggableScrollableSheet` 小部件来实现这样的效果。它通常用作页面的底部或侧边面板,用户可以通过拖动该面板来展示更多内容。 以下是一个简单的示例代码,演示了如何使用 `DraggableScrollableSheet`: ```dart @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('DraggableScrollableSheet'), ), body: Stack( children: <Widget>[ // 页面主体内容 ListView.builder( itemCount: 100, itemBuilder: (context, index) => ListTile( title: Text('Item $index'), ), ), // 可拖动的滚动面板 DraggableScrollableSheet( builder: (context, scrollController) => Container( color: Colors.grey[300], child: ListView.builder( controller: scrollController, itemCount: 20, itemBuilder: (context, index) => ListTile( title: Text('Sheet Item $index'), ), ), ), ), ], ), ); } ``` 在这个示例,页面主体内容由一个 `ListView.builder` 构成,其包含了一些简单的列表项。而 `DraggableScrollableSheet` 是一个具有固定高度的容器,里面包含了另一个 `ListView.builder`,用于展示可拖动面板的内容。 你可以根据需要自定义 `DraggableScrollableSheet` 的样式和行为,例如通过设置高度、背景颜色、滚动控制器等。希望这个示例能帮到你!如果有更多问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值