最近接手了一个flutter项目,整体感觉代码质量不高,感觉有些是初学者容易犯的问题。几年前写的前三篇,我是站在我自己开发遇到问题的角度,这篇是站在别人遇到问题的角度,算是一种补充。下面我整理一下遇到的小问题,大家可以当作开发中的Tips。
1.使用“平替”Widget
Spacer
有时候在Row
或者Column
中需要占位会有如下写法:
Expanded(child: Container())
/// 或
const Expanded(child: SizedBox())
上面两种相比我更推荐第二种,因为可以加const
。另外第一种方式,因为Container
没有child,所以它的大小会撑满父布局。在Stack
中使用需要注意,别问我为什么突然这么说,因为我就遇到这样写的,然后出了问题。。。
当然Flutter还有自带的Spacer
,源码如下:
class Spacer extends StatelessWidget {
const Spacer({super.key, this.flex = 1})
: assert(flex > 0);
final int flex;
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
}
等于是上面第二种方法封装了一层,使用起来也更方便,const Spacer()
就搞定了。
Nil
有时会根据一个条件来判断显示什么widget。当我们无法返回null时,我们会返回类似const SizedBox()
的东西。
这很好,但自从
SizedBox
创建RenderObject
,它有一些性能影响。RenderObject
位于渲染树中,并在上面执行一些计算,即使它在屏幕上没有绘制任何东西。
所以我们可以有一个不创建RenderObject
的Widget,同时仍然有效。Nil小部件是它的实现。它只创建一个Element
,在构建时什么也不做。
代码如下:
/// A widget which is not in the layout and does nothing.
/// It is useful when you have to return a widget and can't return null.
class Nil extends Widget {
/// Creates a [Nil] widget.
const Nil({super.key});
Element createElement() => _NilElement(this);
}
class _NilElement extends Element {
_NilElement(Nil super.widget);
void mount(Element? parent, dynamic newSlot) {
assert(parent is! MultiChildRenderObjectElement, """
You are using Nil under a MultiChildRenderObjectElement.
This suggests a possibility that the Nil is not needed or is being used improperly.
Make sure it can't be replaced with an inline conditional or
omission of the target widget from a list.
""");
super.mount(parent, newSlot);
}
bool get debugDoingBuild => false;
void performRebuild() {
super.performRebuild();
}
}
目前Nil
不支持Row
、Column
这类(MultiChildRenderObjectElement
)多个子节点的组件。
ColoredBox/SizedBox/DecoratedBox等
如果只是设置背景色,可以完全使用ColoredBox
替代Container
;同理如果只是设置大小,可以使用SizedBox
;只设置边框圆角渐变这类,可以使用DecoratedBox
。类似的还有Padding
,Transform
等。如果你需要以上的多种组合,这个时候推荐Container
,因为它就是以上实现的封装。
为什么如此?Container
是一个比ColoredBox
等更重的小部件,里面的实现不都是我们需要的。同时也是为了尽可能的使用const
声明。const
的问题,我在很早之前的Flutter性能优化实践 —— UI篇中就有说明过:
有人测试一个页面上构建1000个重复图标,结果使用const构造函数的,FPS大约高8.4%,内存使用量降低约20%。
当然,实际一个页面上有1000个Widget也不现实。其实说这个点的原因也是希望大家能养成一个好习惯。
如果你的linter启用了use_colored_box或是sized_box_for_whitespace,当你有上述“错误”写法时,编译器也会给你相应的警告。
2.嵌套
Flutter的地狱嵌套一直被许多人诟病,觉得代码看起来很乱。但是实际上很好解决这个问题。
- Widget封装。封装公共组件,实现拆分。
- 善用
ThemeData
,可以避免重复设置属性。 - 可以使用flutter_constraintlayout 库,它和 Android 下的
ConstraintLayout
,iOS 下的AutoLayout
相似。可以将“阶梯状”的代码变得“扁平”。我自己在日常开发中优先就使用的它,可以让我无脑布局。 - 换一种嵌套方式。大家可以看下Flutter的源码,学习一下代码风格。例如
Container
源码:
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null) {
current = Padding(padding: effectivePadding, child: current);
}
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
...
if (margin != null) {
current = Padding(padding: margin!, child: current);
}
if (transform != null) {
current = Transform(transform: transform!, alignment: transformAlignment, child: current);
}
return current!;
}
可以看到是一种从上到下嵌套的方式。比如我们写一个控件,可以先写内容,然后再套大小间距样式,再套点击事件这种。这样代码就会清晰很多。下面是一个简单的例子:
Widget build(BuildContext context) {
Widget child = Row(
children: <Widget>[
Text(title),
const Spacer(),
...
],
);
child = Container(
margin: const EdgeInsets.only(left: 15.0),
padding: const EdgeInsets.fromLTRB(0, 15.0, 15.0, 15.0),
constraints: const BoxConstraints(
minHeight: 50.0,
),
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, width: 0.6),
),
),
child: child,
);
return InkWell(
onTap: onTap,
child: child,
);
}
3.Linter规则
使用linter可以帮助我们识别Dart代码中可能存在的问题,当有“问题”时,编译器会有警告,同时里面会有文档链接详细解释说明。另一方面在团队开发中也可以统一代码规范。
一般新建项目后,会自动添加flutter_lints
库。这个里面有lints库中有关dart的core
规则集,和recommended
规则集,加上flutter_lints
中的flutter规则集。我数了一下大概八十多条规则,但我觉得还远远不够。比如上面提到有关ColoredBox
的use_colored_box
这条就没有。
所以我个人推荐使用Flutter项目中的analysis_options.yaml
内容来替换flutter_lints
,然后可以基于此规范再做调整。目前Flutter项目中的analysis_options.yaml
大致有两百条规则左右。更多的规则可以给到更多代码建议,同时统一标准,长远看是有利于团队的。
比如最近我看见analysis_options.yaml
中新增了一条规则strict-inference,它是当无法确定静态类型时,类型推断永远不会选择动态类型。
加上这条后,我就发现了我之前的“错误”写法。
就是因为我没有声明方法的返回类型,那么以前编译器就会推断为dynamic
。所以修改很简单,前面加上void
就好了。
其实看下flutter源码,你就会发现都是很标准的写法:
所以对于初学者我更是建议学的时候就直接上高标准,学的时候就严要求自己。否则“不良”的代码习惯一旦养成,后面再改就比较痛苦了。
4.SafeArea
SafeArea
内部通过Padding
添加间距来避免我们的widget和状态栏/刘海/底部的安全区域重叠。但是使用时需要注意,默认情况下SafeArea
的上下左右四个属性都是true。所以如果包裹的控件在上方显示,注意将bottom
改为false,否则如果设备bottom不为0时,包裹的控件下方会有高度占用,影响下方排列的widget。
另外就是横屏使用时,注意考虑左右方向。
5.Mixin
Mixin
(混入)是Dart语言的一个特性。有别于extends的单继承,它是一种在多类层次结构中复用代码的一种方式。我个人认为是必须要掌握的,使用起来方便,也是封装功能的首选。我们平时使用的SingleTickerProviderStateMixin
、AutomaticKeepAliveClientMixin
都是利用Mixin
实现的。
下面我举个小例子说明一下用法。比如平时会使用各种Controller
来监听动画,滑动之类的,页面销毁时我们要将这些Controller
释放掉。就会有类似下面的代码:
class TestPageState extends State<TestPage> {
final TextEditingController _controller = TextEditingController();
final FocusNode _nodeText = FocusNode();
void initState() {
_controller.addListener(callback);
super.initState();
}
void dispose() {
_controller.removeListener(callback);
_controller.dispose();
_nodeText.dispose();
super.dispose();
}
}
如果一个页面上有许多Controller
时,会写许多这样的代码。说实话挺麻烦的,一个不留神可能还会漏掉。那我们完全可以把这些相似的操作封装起来。
mixin ChangeNotifierMixin<T extends StatefulWidget> on State<T> {
Map<ChangeNotifier?, List<VoidCallback>?>? _map;
Map<ChangeNotifier?, List<VoidCallback>?>? changeNotifier();
void initState() {
_map = changeNotifier();
/// 遍历数据,如果callbacks不为空则添加监听
_map?.forEach((changeNotifier, callbacks) {
if (callbacks != null && callbacks.isNotEmpty) {
void addListener(VoidCallback callback) {
changeNotifier?.addListener(callback);
}
callbacks.forEach(addListener);
}
});
super.initState();
}
void dispose() {
_map?.forEach((changeNotifier, callbacks) {
if (callbacks != null && callbacks.isNotEmpty) {
void removeListener(VoidCallback callback) {
changeNotifier?.removeListener(callback);
}
callbacks.forEach(removeListener);
}
changeNotifier?.dispose();
});
super.dispose();
}
}
在页面执行initState
和dispose
方法时,就会自动执行Mixin
中的这两个方法,就像是方法“混入”其中一样。
修改后,使用方法:
class TestPageState extends State<TestPage> with ChangeNotifierMixin<TestPage> {
final TextEditingController _controller = TextEditingController();
final FocusNode _nodeText = FocusNode();
Map<ChangeNotifier, List<VoidCallback>?>? changeNotifier() {
return {
_controller: [callback],
_nodeText: null,
};
}
}
这样处理后,代码是不是简洁了许多。而且使用起来只需要添加ChangeNotifierMixin
实现changeNotifier
方法就行了。如果用抽象去封装,效果可以达到一样,但是无法多个继承,都放在一个base下会越来越臃肿。Mixin
有种即插即用的感觉,既可以像继承一样方便,又可以像接口一样实现多个,非常好用。
同理,Dart的扩展方法,枚举扩展这些语法特性,都是需要掌握的。
本篇到此结束,同时推荐阅读前三篇。后面有补充内容也会更新到这里。