【Flutter】实战问题集锦(一)

5,010 阅读7分钟

问题集每篇记录设置在十个左右。绝大多数都是自己在Flutter实际开发中遇到的情况和解决办法,加深在开发过程中面对问题的思考和思路以及日后方便查阅做备份同时也希望能为大家带来帮助。

Flutter中文字和英文字高度显示不同

这就存在一个问题,在UI布局如果为Text设置Padding的时候会出现中英文文本显示高度不一致的情况。难怪设计师老是找我茬,总算知道问题所在了。 解决办法其实很简单在TextStyle中将height设为1就能可以了。

TextStyle(
    fontSize: 15,
    fontFamily: "",
    height: 1,
)

实际中会发现中文显示会超出实际高度,但这里我们是为了保持统一高度以便于对外边框做设置,所以超出部分并不影响正常使用。

滑动组件在Android和iOS不同表现

由于Android和iOS平台UI规范不同导致滑动组件交互性存在差异。例如在Android平台在边界是波纹动画交互而iOS平台边界是回弹阻尼效果。所开发者希望统一交互效果可用ClampingScrollPhysics(Android)或是BouncingScrollPhysics(iOS)两者替换SingleChildScrollView的physics。

SingleChildScrollView(
    physics: BouncingScrollPhysics(), ///ClampingScrollPhysics
)

设计师请不要切这么多Icon

Android的ImageView有个属性为tint可以渲染图片颜色,但前提是渲染的图片为单色。那么在Flutter中Iamge的color也是同样的作用,一方面可以减少切图量一方面方便开发后期调色。之前一些Icon在可点击和不可点击呈现颜色是不同,UI在切图上是同样图片切了两份给我,由于切图非单色导致渲染图片颜色失败。

Image.asset(
   "res/drawable/ic_circle_company.png",
   height: 40,
   width: 40,
   color:Colors.red,
),

单色切图可以通过设置不同颜色实现颜色替换,所以有状态的图标尽量让设计师切成单色剩下交给开发者自由发挥。

虽然这不算是真正的开发问题,但也算是开发过程中遇到的有意义的问题所以还是想记录一下,对于重复和可优化的点我们是不是应该尽量最简化呢。另外减少冗余资源占用降低应用包大小。

关于路由不要只会用pop了

timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
  if (num == 0) {
    timer.cancel();
    Navigator.of(context).pop();
    bus.emit(EventBus.privateEvent);
  } else {
    num--;
    setState(() {});
  }
});

如上代码闪屏页倒计时关闭逻辑,因为还存在其他异步的业务逻辑比如在闪屏页面出现登录退出弹出登录页面。然后倒计时结束调用Navigator.of(context).pop时退出的是路由栈最上登录页,导致路由栈页面出现问题无法手动退出闪屏页同时进入不到首页(预先闪屏页已经设置了禁止退出)。因此使用removeRoute用于关闭当前页面。

了解更多关于Flutter路由可以看看Flutter实战之路由功能篇

timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
  if (num == 0) {
    timer.cancel();
    Navigator.of(context).removeRoute(ModalRoute.of(context));
    bus.emit(EventBus.privateEvent);
  } else {
    num--;
    setState(() {});
  }
});

TextField使用真要命

在特殊情况下使用TextField问题,例如在Row嵌套TextField要有Expanded约束宽度。

展示布局失败

一些特殊情况会导致布局构建失败,例如使用外层加入Row包装时会报错。

Row(
  children: <Widget>[
    Image.asset("res/img/ic_star.png"),
    TextField(
        cursorColor: Colors.grey,
        decoration: InputDecoration(
          hintText: "HHHHHH",
          border: InputBorder.none,
          contentPadding: EdgeInsets.all(0),
          isDense: true,
        ),
      ),
  ],
)

An InputDecorator, which is typically created by a TextField, cannot have an unbounded width 这时只能在TextField外层嵌套Expanded解决。

Row(
  children: <Widget>[
    Image.asset("res/img/ic_star.png"),
    Expanded(
      child: TextField(
        cursorColor: Colors.grey,
        decoration: InputDecoration(
          hintText: "HHHHHH",
          border: InputBorder.none,
          contentPadding: EdgeInsets.all(0),
          isDense: true,
        ),
      ),
    ),
  ],
)

获取焦点弹起软键盘

通常情况输入框默认获取焦点弹起软键盘的方式。

TextField(
  autofocus: true,
)

但也存在输入框焦点默认还是未获取软键盘没弹出的情况,那么再通过设置focusNode实现。

SchedulerBinding.instance.addPostFrameCallback((_) {
   FocusScope.of(context).requestFocus(_focusNode);
});
TextField(
   autofocus: true,
   focusNode: _focusNode,
)

最后在退出页面时记得焦点移除收回软键盘

FocusScope.of(context).requestFocus(FocusNode());

输入框与其他组件显示高度问题

因为原生输入框样式并不满足日常业务开发并自定义输入框采用了Container嵌套输入框增加外边框样式。如下代码导致输入框和其他组件高度不再统一水平线上。

Container(
  margin: EdgeInsets.symmetric(horizontal: 10),
  padding: EdgeInsets.symmetric(horizontal: 10),
  alignment: Alignment.centerLeft,
  height: 30,
  decoration: new BoxDecoration(
    color: Colors.blueGrey,
    borderRadius: new BorderRadius.circular(4),
  ),
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Image.asset(
        "res/img/ic_star.png",
        width: 10,
      ),
      Expanded(
        child: TextField(
          cursorColor: Colors.grey,
          autofocus: true,
          decoration: InputDecoration(
              hintText: "HHHHHH",
          ),
          showCursor: true,
          textInputAction: TextInputAction.search,
        ),
      ),
    ],
  ),
),

最后设置如下代码解决,isDense为true、contentPadding都设为0。

TextField(
  cursorColor: Colors.grey,
  decoration: InputDecoration(
      hintText: "HHHHHH",
      border: InputBorder.none,
      contentPadding: EdgeInsets.all(0),
      isDense: true,
      hintStyle: TextStyle(fontSize: 15)),
  style: TextStyle(fontSize: 15),
  showCursor: true,
),

Flutter网络请求无法抓包情况

测试同学说我开发应用无法抓包,想对测试数据进行监控和问题排查带来困惑。事实上是Flutter开发中网络请求默认无法直接抓包。

Flutter升级到V1.12.3 iOS包大小问题

之前开发一直使用FlutterV1.9的版本,因为FlutterBoost有升级到支持V1.12.3-hotfix所以想作死一把将Flutter更新上去。前期升级很顺利没遇到啥问题基本上无任何需要修改配置的地方,直到在进行iOS打包时问题发生了。原先iOS打包后包大小在30M左右,现在包大小在100M左右。

确认再三我打包为release版本后赶紧在github上看issue,果真遇到这个问题的人不只我一个。该问题不是Flutter的bug而是1.12版本开始Flutter引擎包含了LLVM IR (bitcode)导致包大小发生变化。

同时将ios打包的ipa文件上传到App Store Connect后平台会自动将安装包压缩至正常大小。但目前还不了解如何做到不上传到store也把包大小压缩下来,后期有时间再研究研究。

页面路由返回值处理

页面返回除了appbar的返回键外还有侧滑操作。在侧滑操作返回通过路由传递参数同样需要拦截侧滑返回操作自定义路由弹出操作传递需要的参数。这里就用到WillPopScope组件将onWillPop返回值设为Future.value(false),然后自定义设置回参值。因此若有业务需求都需要返回值时则要特别注意这一点。但你如果使用了redux全局状态管理估计可以忽略这个问题了😂

但从另一方面来看通过路由方式传参并不是特别酸爽,一些特殊情况下还需要考虑多种情况。或许全局状态管理确实是另一种更好的选择。

PS: 同时WillPopScope也适用于loading加载时禁止侧滑退出等操作

onWillPop: () {
    Navigator.of(context).pop({
      "code": industryCode,
      "filter": vm.filters.length > 0 ? vm.filters : "",
    });
    return Future.value(false);
  },

iPhoneX底部黑棒棒适配

底部导航栏默认支持iOS底部横条的适配,但自己写的布局就可能存在问题啦。例如使用Stack在底部Position组件很有可能组件显示内容被横条挡住。

Scaffold(
  appBar: AppBar(),
  body: Container(
    color: Colors.green,
    child: Stack(
      children: <Widget>[
        Positioned(
          bottom: 0,
          child: Text("LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"),
        )
      ],
    ),
  ),
);

查看BottomNavigationBar源码会发现下面一段代码,通过MediaQuery.of(context).padding获取屏幕底部内边距,在BottomNav布局中设置Padding来避免被横条遮挡。

  // Labels apply up to _bottomMargin padding. Remainder is media padding.
    final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - widget.selectedFontSize / 2.0, 0.0);
/// 省略了部分无用代码 
.... 
Padding(
  padding: EdgeInsets.only(bottom: additionalBottomPadding),
  child: MediaQuery.removePadding(
    context: context,
    removeBottom: true,
    child: _createContainer(_createTiles()),
  ),
 ),

因此对ios横条适配需要对距底部的组件在加上MediaQuery.of(context).padding.bottom间距。当然对于iPhoneX的bottom值才大于0,像iPhone6获取到的bottom值则为0以及android也是如此(具体情况看手机型号和机型而定)。