阅读 18107

Flutter 开发踩坑记录(干货总结)

flutter.png

Flutter 太好学了!BUG 真的太少了! issues 只有 5000 多!也就那么亿点!简单得我都枯了!毕竟每次遇到问题,👴🏻 都是直接去找群里的法佬、低调、Alex 等几位大佬(🐶管理,此处小声哔哔)来解决,只要有大佬在,问题也就不大。虽然法佬经常说要学会看源码,但道理大家其实都懂,看源码也就图一乐,真正有 BUG 还是得找法佬。

不多哔哔,单写一篇文章,先记录它一手。本文记录 👴🏻 在 Flutter 开发中遇到的一些 BUG(as design),避免遗忘,如果正在看文章的你也遇到了,那咱们可以握个手。

容器宽高相关问题

Container 设置宽高不生效

一般是由于父级容器的 constraints 属性引起的,在 Flutter 中,子组件的大小会被父组件的 constraints 属性限制,例如

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100.0, // 最小宽度为 100 像素
    minHeight: 50.0 // 最小高度为 50 像素
  ),
  child: Container(
    height: 5.0,// 高度为 5 逻辑像素
    child: redBox 
  ),
)
复制代码

上面的代码中,Container 组件设置高度为 5 像素,是无法生效的,因为父级容器已经设置了最小高度为 50 像素,所以 Container 组件的最终高度将会是 50 像素。

当然,这肯定不是我们想要的效果,我们就想让 Container 组件的最终高度是 5 像素怎么办?其实很简单,可以使用 UnconstraindBox 解除父级容器的 constraints 属性对子组件大小的限制。例如:

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100.0, // 最小宽度为 100 像素
    minHeight: 50.0 // 最小高度为 50 像素
  ),
  child: UnconstraintsBox(
    child: Container(
      height: 5.0, 
      child: redBox 
    ),
  ),
)
复制代码

UnconstrainedBox 允许其子组件按照其自身的大小绘制,我们很少直接使用此组件,除非对于 Material 自带的一些组件,如 Appbar 的 icon 被官方限制了固定的大小,利用该组件可以解除限制,而一般情况下,我们在组件外面套一层布局类组件就可以解决需求,例如以下组件:

Row()
Column()
Align()
Center()
Flex()
Wrap()
Flow()
Stack()
复制代码

SignleChildScrollView 不满一屏高度时无法撑满全屏

其实和上面这个问题是相似的,可以使用布局类组件解决,或者用如下方式:

Container(
  alignment: Alignment.topLeft,
  child: SingleChildScrollView(),
),
复制代码

如果你看过 Container 的源码你会发现其实设置 alignment 属性,和用 Align 组件是一回事,源码也是使用 Align 组件,这就是个语法糖,仅此而已。

说到语法糖,其实 Center 组件也是 Align 组件的语法糖,当你不给 Align 传递任何参数时,使用 Center() 和使用 Align() 是一模一样的效果,我的习惯是不管什么情况,都是只用Align 组件。

如何自定义 AppBar

上文提到过,Flutter 官方对 AppBar 的限制非常严格,连基本的高度都被写死了,这怎么能满足我们项目锦鲤所提出的花式需求呢?所以我在项目中除了使用了自带的 SliverAppBar,其他相关的 AppBar 组件基本没用。

自定义 AppBar 有两种方式:

第一种方式,使用 ColumnExpanded 组件,提供我项目中的一个简单示例:

class VideoEditPage extends StatelessWidget {
  get appBar => DecoratedBox(
    decoration: BoxDecoration(
      color: currentTheme.primaryColor,
      boxShadow: tabBoxShadow,
    ),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: navigatorState.pop,
              child: Container(
                width: Screens.appBarHeight,
                height: Screens.appBarHeight,
                margin: EdgeInsets.only(right: setWidth(7)),
                child: Icon(
                  IcoMoon.arrowLeft,
                  size: setWidth(42),
                  color: currentTheme.primaryColorDark,
                ),
              ),
            ),
            Text('编辑视频', style: titleStyle),
          ],
        ),
        GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: Routes.pushUploadCoverEditPage,
          child: Container(
            height: Screens.appBarHeight,
            alignment: Alignment.center,
            padding: EdgeInsets.symmetric(horizontal: setWidth(19)),
            child: Text('下一步', style: titleStyle),
          ),
        ),
      ],
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: <Widget>[
            appBar,
            Expanded(child: listView)
          ],
        ),
      ),
    );
  }
}
复制代码

第二种方式,使用 StackPositioned 组件,示例:

class MyApp extends StatelessWidget {
  get body => Stack(
    children: <Widget>[
      appBar,
      Positioned(
        left: 0,
        top: Screens.appBarHeight,
        right: 0,
        bottom: bottomAppBarHeight,
        child: listViewBox,
      ),
      bottomAppBar,
    ],
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SizedBox.expand(child: body),
      ),
    );
  }
}
复制代码

Container 设置 borderRadius 不生效

设置 borderRadius 有两种做法,第一种使用 Container 等组件自带的 borderRadius 属性,第二种是,直接用 ClipRRect 等 clip 组件对容器进行裁剪,第二种比第一种更加暴力、消耗性能,但更有效。

例如给 TabView 的容器设置 borderRadius,你会发现无法生效,而使用 ClipRRect 则可以解决,我的理解是 ClipRRect 会直接裁剪成圆角形状,而 BorderRadius 的圆角外的弧形范围是透明的,类似 css 中的 display:noneopaticy:0 的区别,实际具体是什么原因,我也没有去细究,复制粘贴、能跑就行。

列表高度位置发生变化

TabBar 切换导致 PageView、ListView 等滚动组件位置改变

解决办法:给滚动组件加上 key 属性,用于保存位置信息,例如: key: PageStorageKey(1)

其实一般的 ListView 还无法满足我们日常开发中各种花式的需求,推荐使用法佬的 NestedScrollView

法佬已经给我们解决了很多奇怪的 BUG,还要什么自行车?

push 页面返回后,导致页面重新 build 而引起位置变化

遇到这种情况很好解决,使用 StatefulWidget 混入 AutomaticKeepAliveClientMixin 保持页面状态,当路由 pop 后就不会引起重新 build

例如:

class GalleryPage extends StatefulWidget {
  @override
  createState() => GalleryPageState();
}

class GalleryPageState extends State<GalleryPage> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(body: GridView.builder());
  }    

  @override
  bool get wantKeepAlive => true;
}
复制代码

如上所示,如果在 GalleryPagepush 进一个新页面,再 pop 返回 GalleryPage 页时,状态会保持不变,列表不会重新 build

元素显示层级问题

可以认为 Flutter 中 widget 布局的层级关系是递进的,例如 child 的层级比父 Widget 层级更高, ColumnRow 等组件的 children 中同级 widget,谁在后面谁的层级就更高,和 Stackchildren 的层级关系相同。

显示隐藏的几种做法

第一种,利用 IndexedStack 组件控制层级,上面也提到过,子组件谁在后面谁的层级就高,Flutter 中虽然没有 z-index 这一说法,但其实原理和 css 的 z-index 是类似的,index 越大,层级越高,当然这里的 IndexedStackindex 属性是用来控制当前显示的某一个 children,只能显示一个。该方法常用于 APP 首页切换底部导航。

第二种,利用 IgnorePointerOpacity 组件组合隐藏 widget,可以使用 AnimationOpacity 组件达到以前 JQuery 中常用的 fadeIn 效果。

第三种,利用 PositionedTransform.translate 移动到屏幕外,需要显示时再移动回来,这种做法非常适合动画切换,例如视频进度条等效果。

第四种,利用 Offstage 组件,前三种都是利用视觉效果将元素隐藏起来,其实在布局上并未发生改变,而此组件就是类似于 css 中的 display:none,直接让元素在布局中隐藏,不会在布局上继续占用空间。

最后一种,在 build 方法中提前判断,不符合条件直接不渲染,或者返回空 box,这就类似于 HTML 中删除 dom 元素,我人没了,还显示个🔨,这是最恐怖的。

GestureDetector 设置 onTap 不生效

Listener 默认的 behaviorHitTestBehavior.deferToChild

如果 Listener 的子组件是一个 Container,这个 Container 不设置 decoration 的情况下,即透明背景色、无边框,则点击 Container 时,无法触发 down、up 等事件。

同理,GestureDetector 是对 Listener 的封装,无法触发 onTap 等事件也是必然的,那么解决办法也很简单,有以下两种解决办法:

1. 给 Container 设置 decoration
2. 将 behavior 属性设置为 opaque 或 translucent
复制代码

调用 setState 或 markNeedsBuild 后报错

第一种报错

setState() or markNeedsBuild() called during