flutter嵌套过深的解决方案

2,054 阅读6分钟

引子

前端/ 原生开发者在初转 Flutter时都有一个统一的印象,Flutter的嵌套地狱太深了,刚开始写组件就是一阵恶心,再 review 一次昨天的晚饭都要吐出来了,但是时间长了也就习惯了。

刚开始写flutter时,恶心的原因很多都是基于 对flutter的编程思维的不熟悉,传统的 Android开发,其实也是类似的写法,一个布局xml内部也是嵌套结构,再配合组件的一些内部函数来设置padding等特性,只是它封装组件的方式并没有像flutter做的那么绝,flutter把 Padding 这种属性都独立成了一个组件,要想加内间距,你得往外面再套一层,这种思想上的转变,让原生安卓开发者需要不短的时间来适应。

作为一个成功转型的Flutter开发者,本人说明一下,flutter的嵌套确实是布局的写法 设计上的一种缺陷,但是并不是无解的。嵌套太多层确实容易造成可读性很差,但是 Flutter中一个组件就像相当于一个段落,组件的呈现就相当于读者的阅读效果, 写代码时为了让人看懂,不是写的冗长的一坨翔让人不愿意看第二遍。代码的可读性,还是掌握在编程者自己手里。

比如说,一页屏你一个组件全包含,什么东西都拼在一起。你要把整个页面看完,你得鼠标滚轮滚好久。修改一个 UI 文本内容,光找到这个显示的组件就花了2分钟,如果要重构整个页面,那现有的代码你得全部抛弃,重新再堆一遍了,如此写法,工作效率极低。

比如下面的代码

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('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.phone),
                    Text("amy"),
                  ],
                ),
              ),
              Container(
                color: Colors.white,
                padding: EdgeInsets.all(20),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.phone),
                    Text("billy"),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
​

解决方案

提取,封装,复用

其实要解决嵌套层次太深的问题,基本上用以上几个字可以解决 。

很容易理解,就是我们常用的 面向对象编程思维方式,

  1. 按照零件的维度去把某一层嵌套提取出来,形成一个 返回值为Widget的函数,这个函数可以是全局的,也可以是非全局的(Dart语言里面,函数是一等公民,我们可以直接把函数定义成一个对象,或者一个类型Typtedef).
  2. 如果这个零件,需要跨页面使用,那么就提取成一个 继承了StatefulWidget 或者 StatelessWidget类 的 组件,能够在整个项目中复用。
  3. 如果要跨项目使用,那就发布到内网gitlab或者pub_dev平台,在项目中引入使用。

经过初步提取之后,上述代码至少可以缩短10行:

![16f51065d951fe5e_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0](../16f51065d951fe5e_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.webp)class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
​
  Widget _myContainer(String content) {
    return Container(
      color: Colors.white,
      padding: const EdgeInsets.all(20),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          const Icon(Icons.phone),
          Text(content),
        ],
      ),
    );
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Demo'),
        ),
        body: Container(
            child: Offstage(
                offstage: false,
                child: ListView(
                  children: <Widget>[_myContainer('abc'), _myContainer('def')],
                ))));
  }
}

细心观察的话,上面的改动除了进行组件提取之外,还将一些不必要的末尾逗号去除,这样可以在格式化代码时占用更少的行数。

扩展函数

大部分场景下,我们进行组件的提取封装复用,能够满足多人团队开发的提效需求,然而如果有更高的追求,想要进一步优化编码方式的话,也有一种偏方,先看用法:

以加一个margin外边距为例,按照我们原生同学的习惯应该是:

16f51065d951fe5e_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.webp

但是明显dart不支持这种写法,然而:dart提供了一个扩展函数,能够帮助我们实现上面的写法。扩展函数在 dart2.7版本发布时支持。我们定义一个扩展函数:

/// 表示在 Widget类上进行扩展
extension WidgetExt on Widget {
​
  /// 让所有的widget类拥有一个扩展函数,intoContainer
  Container intoContainer({
    //复制Container构造函数的所有参数(除了child字段)
    Key key,
    AlignmentGeometry alignment,
    EdgeInsetsGeometry padding,
    Color color,
    Decoration decoration,
    Decoration foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    EdgeInsetsGeometry margin,
    Matrix4 transform,
  }) {
    //调用Container的构造函数,并将当前widget对象作为child参数
    //现在当前widget对象就会被包裹一层container
    return Container(
      key: key,
      alignment: alignment,
      padding: padding,
      color: color,
      decoration: decoration,
      foregroundDecoration: foregroundDecoration,
      width: width,
      height: height,
      constraints: constraints,
      margin: margin,
      transform: transform,
      child: this,
    );
  }
}
​

在引入了这个文件之后,我们可以用下面的方式去写代码(AndroidStudio有个bug, 引入这种扩展函数必须手动copy,编译器不会自动导入)

16f51065d951fe5e_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0-16545025346411.webp

如果是需要 用一个list<String>对象,构建一个List<Widget>

我先扩展一个函数:

import 'package:flutter/cupertino.dart';
​
extension WidgetListExt<T extends Widget> on List<String> {
  List<Widget> getWidgetGroup() {
    List<Widget> list = [];
    for (var element in this) {
      list.add(Container(child: Text(element), margin: const EdgeInsets.all(10)));
    }
    return list;
  }
}
​

然后使用它:

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
​
  final String title;
​
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
            child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
            ...['adf', 'asfsaf'].getWidgetGroup()
                // 效果等同于下面的代码
              // Container(child: Text('abc'), margin: const EdgeInsets.all(10)),
              // Container(child: Text('asfsaf'), margin: const EdgeInsets.all(10))
        ])));
  }
}

效果等同于下面的代码

 Container(child: Text('abc'), margin: const EdgeInsets.all(10)),
 Container(child: Text('asfsaf'), margin: const EdgeInsets.all(10))

说是偏方,是因为有争议。

  1. 有人觉得这种写法破坏了 谷歌原本的声明式UI的写法, 初次阅读这种写法的人会觉得 上面的命令式(命令式即 a.xxx(..))写法很奇怪,比如上面的 intoContainer 阅读起来要从又往左读,才能知道是 Container包裹了一个Text子组件. 而普通的flutter写法则是从上往下,从左往右,习惯上对flutter的官方习惯造成了冲突。
  2. flutter本身的const特性可以提升刷新UI时的流畅度减少开销,但是加上这个之后,const`会报错,本组件不再是常量组件,每次刷新都会重新构建,有可能造成一定的性能额外开销。

总结

上述两种方案,个人经验是以第一个方法为主,特殊的组件,可以用扩展函数做成一种简洁的写法,但是扩展函数要悠着点用,因为扩展属于全局,如果太多了,排查问题的范围会很大。

Flutter内部其实有一种类似android的约束布局一样的概念,可以减少层级,但是使用不当也有可能对渲染你性能有影响。