Flutter嵌套优化

2020-12-16  本文已影响0人  半心_忬

对于Native开发的同学刚接触flutter来说,编写代码比较困恼或者说不习惯的地方主要有两个:

命令式编程到声明式编程的思维转变就只能靠自己慢慢适应了,但嵌套地狱,是有方法来优化的。

宠物列表 栗子

我们用一个简单的栗子来循序渐进,如何利用语言特性来逐步优化,提升编码的体验,增强代码的可读性,以及后期的可维护性。

一个简单的需求如下,显示一个宠物的列表,效果如如下:

1_demoPage.png

虽然是一个很简单的栗子,但按原始的写法,我们的代码是这个样子的:

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Nested Demo'),
      ),
      body: Container(
        child: Offstage(
          offstage: false,
          child: ListView(
            children: <Widget>[
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('喵星人'),
                  ],
                ),
              ),
              Divider(
                height: 2,
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('汪星人'),
                  ],
                ),
              ),
              Divider(
                height: 2,
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('呜星人'),
                  ],
                ),
              ),
              Divider(
                height: 2,
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('嗷星人'),
                  ],
                ),
              ),
              Divider(
                height: 2,
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('哞星人'),
                  ],
                ),
              ),
              Divider(
                height: 2,
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.pets),
                    SizedBox(
                      width: 12,
                    ),
                    Text('咯星人'),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

对于从Native转过来接触Flutter的同学来说,这,简直了,就一个嵌套地狱呀...

从代码来看,创建每个显示的item和分割线,都是重复代码,于是自然就会想到抽取重复代码。

拆分嵌套的代码

而当前应对嵌套过深的方式,主流做法,就是对widget进行拆分,把一个大的build方法,拆分为小的widget创建方法。

我们将item的创建和分割线的创建,抽取为私有方法,得到如下代码:

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Nested Demo'),
      ),
      body: Container(
        child: Offstage(
          offstage: false,
          child: ListView(
            children: <Widget>[
              _buildItem("喵星人"),
              _bulidDivider(),
              _buildItem("汪星人"),
              _bulidDivider(),
              _buildItem("呜星人"),
              _bulidDivider(),
              _buildItem("嗷星人"),
              _bulidDivider(),
              _buildItem("哞星人"),
              _bulidDivider(),
              _buildItem("咯星人"),
            ],
          ),
        ),
      ),
    );
  }

  Container _buildItem(String name) {
    return Container(
      color: Colors.white,
      padding: EdgeInsets.all(20),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.pets),
          SizedBox(
            width: 12,
          ),
          Text(name),
        ],
      ),
    );
  }

  Widget _bulidDivider() {
    return Divider(
      height: 2,
    );
  }
}

咋看下来,代码结构和嵌套层级都得到优化,一般对嵌套的优化也就到这一步了。

看_buildItem这个方法,相对来说,嵌套层级是不是还是有点深,看起来不是那么舒适,我们就只能做到这一步吗?

想想,要不把创建Row也抽取一个方法,不过真的有这个必要吗,如果是再复杂一点的需求呢,会拆成茫茫多的方法,到时候优化了嵌套地狱,同时又掉进了维护火葬场。

自定义扩展 —— extension

对于写代码来说,如果在嵌套和.语法之前做选择,相信更多的人都愿意使用.语法,易用也好理解。

以上述的_buildItem方法为例:

Container _buildItem(String name) {
    return Container(
      color: Colors.white,
      padding: EdgeInsets.all(20),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.pets),
          SizedBox(
            width: 12,
          ),
          Text(name),
        ],
      ),
    );
  }

最外层的Container,是否可以使用.语法来进行添加呢?

Dart在2.6.0时支持了extension语法,使.语法来优化嵌套成为了可能。

所以我们来利用这个extension扩展一下widget,添加一个Container嵌套:

// 单个widget的扩展常用Widget
extension SingleWidgetNestedCommonlyUsedExtension on Widget {
  // 嵌套一个container
  Container nestedContainer({
    Key key,
    AlignmentGeometry alignment,
    EdgeInsetsGeometry padding,
    Color color,
    Decoration decoration,
    Decoration foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    EdgeInsetsGeometry margin,
    Matrix4 transform,
    Clip clipBehavior = Clip.none,
  }) {
    return Container(
      key: key,
      alignment: alignment,
      padding: padding,
      color: color,
      decoration: decoration,
      foregroundDecoration: foregroundDecoration,
      width: width,
      height: height,
      constraints: constraints,
      margin: margin,
      transform: transform,
      clipBehavior: clipBehavior,
      child: this,
    );
  }
}

写完该扩展了之后,所有的widget都可以.一个nestedContainer(...)函数,函数的参数与Container的构造函数一致,那么_buildItem方法,就可以写成这样:

Container _buildItem(String name) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.pets),
        SizedBox(
          width: 12,
        ),
        Text(name),
      ],
    ).nestedContainer(
      color: Colors.white,
      padding: EdgeInsets.all(20),
    );
  }

可以通过.语法来添加层级嵌套,大大减少嵌套地狱的可能,也提升了编程体验。

既然Container可以通过extension来变为链式调用,那么其他的单子widget的widget就都可以使用该方式来添加,如下是我封装的
单个widget的扩展,包含如下四个子扩展:

2_single_commonly.png 3_single_uncommonly.png 4_single_gesture.png 5_single_to_list.png

通过上述扩展,我们的代码就可以写成这样子:

class SPShopVisitRecordBottomButtonWidget extends StatelessWidget {
  final VoidCallback onTaped;
  SPShopVisitRecordBottomButtonWidget({Key key, this.onTaped})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          height: 0.5,
          color: CRMColor.separator,
        ),
        ClipRRect(
                borderRadius: BorderRadius.circular(17),
                child: Container(
                  color: CRMColor.primary,
                  child: _createFlatButton(),
                ))
            .nestedContainer(
              height: 34,
              width: 200,
            )
            .nestedCenter()
            .nestedContainer(
              height: 49.5,
            )
      ],
    ).nestedContainer(
      color: Colors.white,
      height: 50,
    );
  }

  /// 创建按钮
  Widget _createFlatButton() {
    return Text(
      '新增拜访',
      style: TextStyle(
        fontSize: 14,
        color: Colors.white,
      ),
    ).nestedFlatButton(
        shape: RoundedRectangleBorder(
            side: BorderSide.none,
            borderRadius: BorderRadius.all(Radius.circular(17))),
        onPressed: onTaped);
  }
}
多子Widget扩展

比如Row、Colum、ListView...等,这些多子Widget的,我们同样希望能够使用.语法来进行链式调用,于是添加了如下扩展:

// 多个widget 添加widget 扩展
extension MultipleWidgetAddWidgetExtension<T extends Widget> on List<T> {
  // 添加一个 widget
  List<Widget> addWidget(Widget widget,
      {AddWidgetAsListType addType = AddWidgetAsListType.behind}) {
    if (addType == AddWidgetAsListType.front) {
      return [widget] + this;
    }
    return this..add(widget);
  }

  // 添加 widget 数组 然后 返回一个list
  List<Widget> addWidgetList(List<Widget> widgets,
      {AddWidgetAsListType addType = AddWidgetAsListType.behind}) {
    if (addType == AddWidgetAsListType.front) {
      return widgets + this;
    }
    return this + widgets;
  }
}
/// widget 嵌套 的list 扩展
extension WidgetNestedListExtension<T> on List<T> {
  // 将list转换为widget数组
  List<Widget> transformToWidgets(Widget Function(T) builder) {
    return this.map<Widget>((item) {
      return builder(item);
    }).toList();
  }
}
// 多个widget嵌套扩展
extension MultipleWidgetNestedExtension<T extends Widget> on List<T> {
  // 嵌套 listView
  ListView nestedListView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    double itemExtent,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  }) {
    return ListView(
      key: key,
      scrollDirection: scrollDirection,
      reverse: reverse,
      controller: controller,
      primary: primary,
      physics: physics,
      shrinkWrap: shrinkWrap,
      padding: padding,
      itemExtent: itemExtent,
      addAutomaticKeepAlives: addAutomaticKeepAlives,
      addRepaintBoundaries: addRepaintBoundaries,
      addSemanticIndexes: addSemanticIndexes,
      cacheExtent: cacheExtent,
      semanticChildCount: semanticChildCount,
      dragStartBehavior: dragStartBehavior,
      children: this,
    );
  }

  // 添加其他多子Widget的扩展,Row、Colum、CustomScrollView、GridView、Stack
使用添加好的扩展优化上述栗子

添加好这些扩展之后,我们的代码可以写成这样:

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Nested Demo'),
      ),
      body: buildContainer(),
    );
  }

  Container buildContainer() {
    // 链式调用
    return [
      _buildItem("喵星人"),
      _bulidDivider(),
      _buildItem("汪星人"),
      _bulidDivider(),
      _buildItem("呜星人"),
      _bulidDivider(),
      _buildItem("嗷星人"),
      _bulidDivider(),
      _buildItem("哞星人"),
      _bulidDivider(),
      _buildItem("咯星人"),
    ].nestedListView().nestedOffstage(offstage: false).nestedContainer();
  }

  Container _buildItem(String name) {
    // 完全链式调用
    return [
      Icon(Icons.pets),
      SizedBox(
        width: 12,
      ),
      Text(name)
    ]
      .nestedRow(crossAxisAlignment: CrossAxisAlignment.center)
      .nestedContainer(color: Colors.green, padding: EdgeInsets.all(20));
  }

  Widget _bulidDivider() {
    return Divider(
      height: 2,
    );
  }
}
更加链式的封装

为了让我们的代码更加符合链式编程风格,再定义一个静态方法

// widget嵌套用widget
class WidgetNestedWidget {
  static Widget addWidget(Widget widget) {
    return widget;
  }
}

完全使用链式调用,最后的代码是这个样子的:

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Flutter Nested Demo'),
        ),
        body: _createBody());
  }

  // 纯链式
  Widget _createBody() {
    return [
      "喵星人",
      'line',
      "汪星人",
      'line',
      "呜星人",
      'line',
      "嗷星人",
      'line',
      "哞星人",
      'line',
      "咯星人"
    ]
        .transformToWidgets((title) {
          if (title == 'line') {
            return Divider(
              height: 2,
            );
          }
          return WidgetNestedWidget.addWidget(Icon(Icons.pets))
              .addWidgetListAsList([
                SizedBox(
                  width: 12,
                ),
                Text(title)
              ])
              .nestedRow(crossAxisAlignment: CrossAxisAlignment.center)
              .nestedContainer(
                  color: Colors.purple, padding: EdgeInsets.all(20));
        })
        .nestedListView()
        .nestedOffstage(offstage: false)
        .nestedContainer();
  }
}
扩展函数与嵌套的混合调用

虽然我们扩展了常用的Widget为链式调用,但是链式和嵌套是可以共存的,它们是可以交叉使用的,并非非此即彼的关系,如下,保留关键部分的嵌套,其他部分使用链式:

/// 创建头部
Widget _createHeader(
    ServiceVisitRecordState state, Dispatch dispatch, ViewService viewService) {
  return state.showEmptyWidget
      ? Container()
      : Row(
          children: <Widget>[
            RichText(
              text: [
                TextSpan(
                  text: '${state.totalCount}',
                  style: TextStyle(
                    color: CRMColor.error,
                  ),
                ),
                TextSpan(text: '个小记'),
              ].nestedTextSpan(
                text: '按条件共筛选出',
                style: TextStyle(color: CRMColor.desc, fontSize: 12),
              ),
            ),
          ],
        ).nestedContainer(
          color: CRMColor.background,
          height: 32,
          padding: EdgeInsets.only(left: 12),
        );
}
一些注意点

上述内容对对代码结构和编码体验带来提升,在扩展和使用时,有几个注意点也需要注意一下:

// TextSpan 嵌套扩展
extension MultipleWidgetNestedTextSpan<T extends InlineSpan> on List<T> {
  // 嵌套一个 TextSpan
  TextSpan nestedTextSpan({
    String text,
    TextStyle style,
    GestureRecognizer recognizer,
    String semanticsLabel,
  }) {
    return TextSpan(
        text: text,
        style: style,
        recognizer: recognizer,
        semanticsLabel: semanticsLabel,
        children: this);
  }
}

所以个人建议,扩展函数的封装是用来解决嵌套过深的问题的,使用扩展函数的同时,需要保留部分关键嵌套层级结构以使得布局的层级结构保持清晰,以免带来代码阅读和维护的困难,扩展函数与嵌套混用,具体使用到什么程度,个人觉得是不要带来麻烦的程度,具体就看个人的选择了。

后记

上述的测试代码和完整的封装,传送门

上一篇 下一篇

猜你喜欢

热点阅读