前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 组件集录 | 从图标按钮看组件封装

Flutter 组件集录 | 从图标按钮看组件封装

作者头像
张风捷特烈
发布2022-10-31 18:12:10
1.1K0
发布2022-10-31 18:12:10
举报

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 4 天,点击查看活动详情


1. 封装的目的

虽然 Flutter 中提供的组件众多,但并非所有组件都是复杂的。大部分是 StatelessWidgetStatefulWidget 的派生类,在面对这些组件时,我们要清楚地认识一点:

它们的核心功能是基于 已有组件封装构建逻辑,完成特定的功能,简化使用。

比如下面的 BackButtonIcon 组件,继承自 StatelessWidget 。在 build 方法中封装构建逻辑,其中使用 Icon 组件,根据不同的平台,展示不同的图标,如下所示:

代码语言:javascript
复制
---->[BackButtonIcon 源码]----
class BackButtonIcon extends StatelessWidget {

  const BackButtonIcon({ super.key });

  static IconData _getIconData(TargetPlatform platform) {
    switch (platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return Icons.arrow_back;
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        return Icons.arrow_back_ios;
    }
  }

  @override
  Widget build(BuildContext context) => Icon(_getIconData(Theme.of(context).platform));
}

可能有人会疑惑 :

为什么这么简单的逻辑,还要通过一个组件来完成。会不会有些小题大做? 这难道不是导致 Flutter 组件数量庞大的 "元凶" 吗?

对于编程者来说,写重复代码是很反感的。试想一下,如果不用 BackButtonIcon 来封装这个构建逻辑。那么 每次 想要实现不同平台展示不同返回按钮时,就需要编程者自己处理构建逻辑。如果需要修改构建逻辑,就要一处处去修改。

所以 封装 的魅力在于:集中逻辑处理,统一使用的形式,便于复用。 如果一个构建逻辑相对固定,有使用的场景,就可以通过 StatelessWidgetStatefulWidget 进行封装,简化使用。反过来,源码中封装的组件,也一定有其目的或使用场景,我们可以通过这个线索来思考组件的源码设计者的考量。


2. BackButton 组件

BackButton 组件继承自 StatelessWidget ,在 build 构建逻辑中使用 IconButton 组件触发点击事件,如果未提供 onPressed 参数,则会触发 Navigator.maybePop 返回。 显示的内容组件为 BackButtonIcon ,说明其会根据平台来决定图标样式。

另外,可以通过 color 入参设置返回按钮的颜色。通过 源码可以知道,本质上这个颜色属性是传入到 IconButton 组件构造方法中的。

image.png
image.png
代码语言:javascript
复制
class BackButton extends StatelessWidget {
  const BackButton({ super.key, this.color, this.onPressed });

  final Color? color;

  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    return IconButton(
      icon: const BackButtonIcon(),
      color: color,
      tooltip: MaterialLocalizations.of(context).backButtonTooltip,
      onPressed: () {
        if (onPressed != null) {
          onPressed!();
        } else {
          Navigator.maybePop(context);
        }
      },
    );
  }
}

通过已存在的 IconButtonBackButtonIcon 组件,组合拼装成更复杂的,具有某种使用场景的组件。这就是封装后可复用的魅力。如果想对一个组件从根源上进行了解,查看它的构建逻辑即可。从中你可以知其然,知其所以然,当你知道一件事物的构成机理,那它的任何表现都不会脱离你的控制,在使用时就是 “降维打击”


3. IconButton 组件

IconButton 是一个具有圆形水波纹点击效果的组件,必须传入一个子组件 icon 和回调函数 onPressed 。效果如下:

24.gif
24.gif

说实话,国内的应用软件基本上不喜欢用 material 风格。对我个人来说,水波纹能给用户一个交互的反馈,本身是比较好的,但一个小小的图标按钮有水波纹,感觉怪怪的。这不得不让图标按钮的占位区域扩大,当多个 IconButton 排列时,如下所示,默认情况下,水波纹区域太大,又会显得拥挤:

image.png
image.png

不过可以通过 splashRadius 来控制水波纹的扩散半径。但在小区域中,当长按时展示水波扩散的动画效果时,手指几乎占据了整个区域,所以整个动画效果呈现的收益并不明显。

34.gif
34.gif
代码语言:javascript
复制
IconButton(
  onPressed: (){},
  color: Colors.blue ,
  icon: Icon(Icons.add ),
  splashRadius: 20,
),

如下,是启用 Material3 的效果,感觉这种水波纹要比 Material2 的好看一些,对于 IconButton 而言,会根据图标颜色显示背景色,长按时也不再是扩散的水波纹,而是背景色的变化。

33.gif
33.gif
代码语言:javascript
复制
MaterialApp(
   theme: ThemeData(
     primarySwatch: Colors.blue,
     useMaterial3: true
   ),
   home: const MyHomePage(),
 );

下面来简单看一下 IconButton 组件的源码实现,首先,它继承自 StatelessWidget ,表明它是基于已有组件封装构建逻辑,从而形成的新组件。 从构造方法中可以到有大量的可配置属性:


如下是非 useMaterial3 时的主要构建逻辑,主题部分使用 ConstrainedBoxPaddingSizedBoxAlignIconTheme 组合构建:

tooltip 非空时会包裹 Tooltip 组件实现长按提示信息的功能,事件响应通过 InkResponse 组件实现。

image.png
image.png

最后说一下 useMaterial3 的处理, 在 IconButton#build 方法中,通过 Theme 数据的 useMaterial3 属性校验是否启用 Material3

image.png
image.png

通过启用 Material3 ,会返回 _SelectableIconButton 完成构建逻辑。如下所示,像 selectedIconisSelectedstyleminSizemaxWidth是为 Material3 风格新加的属性。

代码语言:javascript
复制
---->[IconButton#build]----
if (theme.useMaterial3) {
  final Size? minSize = constraints == null
      ? null
      : Size(constraints!.minWidth, constraints!.minHeight);
  final Size? maxSize = constraints == null
      ? null
      : Size(constraints!.maxWidth, constraints!.maxHeight);
 // 略...
  if (style != null) {
    adjustedStyle = style!.merge(adjustedStyle);
  }
  Widget effectiveIcon = icon;
  if ((isSelected ?? false) && selectedIcon != null) {
    effectiveIcon = selectedIcon!;
  }
  Widget iconButton = IconTheme.merge(
    data: IconThemeData(
      size: effectiveIconSize,
    ),
    child: effectiveIcon,
  );
 // 略...
  return _SelectableIconButton( // tag1
    style: adjustedStyle,
    onPressed: onPressed,
    autofocus: autofocus,
    focusNode: focusNode,
    isSelected: isSelected,
    child: iconButton,
  );
}

从这里可以看出,通过 IconButton 组件封装构建逻辑,也有利于功能拓展。想要适应新的主题,只要优化构建逻辑即可,这对于使用者是没有任何影响的,且可以通过 Theme 决定启用的主题。组件封装的是 构建流程,它更是一种对 功能的允诺,通过它你能完成什么样的表现,是一个组件最重要的事。


4、FloatingActionButton 组件

FloatingActionButton 一般来说是使用在 ScaffoldfloatingActionButton 属性中。因为 Scaffold 在构建逻辑中有一些和 FloatingActionButton 联动的效果,比如浮动按钮方位、动画等。不过 FloatingActionButton 本身只不过是一个圆形样式的 RawMaterialButton 而已。

它有如下四个构造,用来创建不同类型的浮动按钮,构造中主要为私有的 _FloatingActionButtonType 成员赋值:

image.png
image.png
代码语言:javascript
复制
enum _FloatingActionButtonType {
  regular,
  small,
  large,
  extended,
}

regular

small

large

其中 extendedmaterial3 中的风格,是圆角按钮,可以在官网的 extended-fab 中查看详情,也可以在该网站中看一下其他 material3 的风格:

image.png
image.png

它继承自 StatelessWidget ,表明它是基于已有组件封装构建逻辑,从而形成的新组件。 如下是 FloatingActionButton 的构造方法,其中的属性虽然挺多的,但基本上都是常规属性。其中比较特殊的是 heroTag 属性,默认值是一个 const 常量 _DefaultHeroTag :

代码语言:javascript
复制
class _DefaultHeroTag {
  const _DefaultHeroTag();
  @override
  String toString() => '<default FloatingActionButton tag>';
}
image.png
image.png

如果 heroTag 非空,会在构建逻辑中套上 Hero 组件:

image.png
image.png

这就有一个问题:在界面跳转时同一界面中不能有两个相同 tagHero。这也是一个界面中使用两个 FloatingActionButton 常出现的问题,解决方案也很简单,将手动指定 heroTag 参数即可。

image.png
image.png

最终,FloatingActionButton 是依赖 RawMaterialButton 组件完成展示效果的:

image.png
image.png

从这四个相对简单的 StatelessWidget 组件可以看出,它们本质上是依赖其他已有组件,完成构建逻辑的封装,来满足一些特殊的使用场景。并且通过组件类成员属性的配置,让组件在表现上可以更加灵活。这个就是将构建逻辑分离成组件进行封装的主要优势。

可能有人会疑惑,使用函数不是也能封装组件吗,通过函数参数也能控制构建的表现,它和分离组件有什么区别呢?其实两者在本质上并没有什么区别,目的是一致的:封装特点创建中的构建逻辑。 这个问题等价于在问: 类封装和函数封装的区别

类中可以定义成员变量和成员方法,封装能力更强,更像一个独立的 个体 ,通过类封装相当于加入了 Widget 家族的正规军;通过函数封装,会显得比较零散,不利于分离和管理,但形式的比较灵活,相当于 游击队。至于使用函数还是使用类封装构建逻辑,并没有严格的标准,自己结合场景考量。一般有 复用价值 或后期需要 拓展、修改的构建逻辑,可以独立封装为类,一些临时构建的逻辑,可以通过函数来处理。


更多 Flutter 内置组件介绍,欢迎关注 《Flutter 组件集录》 专栏。

  • @张风捷特烈 2022.10.17 未允禁转
  • 我的 公众号: 编程之王
  • 我的 github 主页 :  toly1994328
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-10-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 封装的目的
  • 2. BackButton 组件
  • 3. IconButton 组件
  • 4、FloatingActionButton 组件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档