引子
前端/ 原生开发者在初转 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"),
],
),
),
],
),
),
),
);
}
}
解决方案
提取,封装,复用
其实要解决嵌套层次太深的问题,基本上用以上几个字可以解决 。
很容易理解,就是我们常用的 面向对象编程思维方式,
- 按照零件的维度去把某一层嵌套提取出来,形成一个 返回值为Widget的函数,这个函数可以是全局的,也可以是非全局的(
Dart语言里面,函数是一等公民,我们可以直接把函数定义成一个对象,或者一个类型Typtedef). - 如果这个零件,需要跨页面使用,那么就提取成一个 继承了
StatefulWidget或者StatelessWidget类 的 组件,能够在整个项目中复用。 - 如果要跨项目使用,那就发布到内网
gitlab或者pub_dev平台,在项目中引入使用。
经过初步提取之后,上述代码至少可以缩短10行:
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外边距为例,按照我们原生同学的习惯应该是:
但是明显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,编译器不会自动导入)
如果是需要 用一个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))
说是偏方,是因为有争议。
- 有人觉得这种写法破坏了 谷歌原本的声明式UI的写法, 初次阅读这种写法的人会觉得 上面的命令式(
命令式即 a.xxx(..))写法很奇怪,比如上面的intoContainer阅读起来要从又往左读,才能知道是 Container包裹了一个Text子组件. 而普通的flutter写法则是从上往下,从左往右,习惯上对flutter的官方习惯造成了冲突。 flutter本身的const特性可以提升刷新UI时的流畅度减少开销,但是加上这个之后,const`会报错,本组件不再是常量组件,每次刷新都会重新构建,有可能造成一定的性能额外开销。
总结
上述两种方案,个人经验是以第一个方法为主,特殊的组件,可以用扩展函数做成一种简洁的写法,但是扩展函数要悠着点用,因为扩展属于全局,如果太多了,排查问题的范围会很大。
Flutter内部其实有一种类似android的约束布局一样的概念,可以减少层级,但是使用不当也有可能对渲染你性能有影响。