1. 保持 build
方法纯净
build
方法必须是纯粹的/没有任何不需要的东西。这是因为有一些外部因素可以触发一个新的小部件构建,下面是一些例子:
-
Route pop/push
-
屏幕大小的调整,通常是因为键盘显示或屏幕方向的改变
-
父部件重新创建了它的子部件
-
Widget 依赖的
InheritedWidget
(Class. of(context)
模式) 发生变化
DON’T:
Widget build(BuildContext context) {return FutureBuilder(future: httpCall(),builder: (context, snapshot) {// create some layout here},);
}
DO:
class Example extends StatefulWidget { _ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {Future<int> future;void initState() {future = repository.httpCall();super.initState();}Widget build(BuildContext context) {return FutureBuilder(future: future,builder: (context, snapshot) {// create some layout here},);}
}
2. 理解Flutter布局约束概念
Flutter布局有一个经验法则,每个Flutter应用程序开发人员都需要知道: 约束向下,大小向上,父元素设置位置。
-
widget有来自其父组件的约束。已知约束是一组包含四个double的集合: 最小和最大宽度,最小和最大高度。
-
接下来,widget将遍历它自己的子列表。widget一个接一个地命令其子widget的约束条件是什么(每个子widget的约束条件可能不同),然后询问每个子widget想要的大小。
-
接下来,widget依次定位它的子widget(水平x轴,垂直y轴)。然后,widget将自己的大小通知其父组件(当然,在原始约束范围内)。
在Flutter中,所有widget都基于它们的父组件或它们的框约束来提供自身。 widget的大小必须在其父组件设置的约束范围内。
3. 使用运算符以减少执行代码的行数
- 使用级联运算符
如果我们要对同一个对象执行一系列操作,那么我们应该选择 ..
运算符:
DON’T:
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();
DO:
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close();
- 使用集合展开运算符
当现有项已存储在另一个集合中时,可以使用展开运算符,展开集合语法会使代码变得更简单。
DON’T:
var y = [4,5,6];
var x = [1,2];
x.addAll(y);
DO:
var y = [4,5,6];
var x = [1,2,...y];
- 使用 Null 安全(
??
)和 Null 感知(?.
)运算符
代码中应该总是将??
(如果为null
)和 ?.
(null
感知)运算符作为第一追求,而不是条件表达式中的null
检查。
DON’T:
v = a == null ? b : a;
v = a == null ? null : a.b;
DO:
v = a ?? b;
v = a?.b;
- 尽量使用“
is
”运算符,而不是使用“as
”运算符
通常,如果无法进行强制转换,则强制转换运算符会抛出异常。为了防止抛出异常,可以使用“is
”。
DON’T:
(item as Animal).name = 'Lion';
DO:
if (item is Animal) item.name = 'Lion';
- 使用字面量初始化可增长集合
Good:
var points = [];
var addresses = {};
Bad:
var points = List();
var addresses = Map();
带泛型的情况:
Good:
var points =<Point>[];
var addresses = <String, Address>{};
Bad:
var points = List<Point>();
var addresses = Map<String, Address>();
4. 仅在需要时使用 Stream
虽然流非常强大,但如果我们使用它们,为了有效地利用这一资源,我们的肩上就有很大的责任。
使用性能较差的Stream可能会导致更多的内存和CPU占用。不仅如此,如果忘记关闭流,还会导致内存泄漏。
因此,在这种情况下,与其使用Stream,不如使用消耗更少内存的东西,例如用于响应式UI的ChangeNotifier。 对于更高级的功能,我们可以使用Bloc库,它将更多的精力放在以有效的方式使用资源上,并提供一个简单的界面来构建响应式UI。
只要流不再被使用,它们就会被有效地清洗。这里的问题是,如果您只是删除变量,这不足以确保它不被使用。它仍然可以在后台运行。
您需要调用Sink.close()
,以便它停止相关的StreamController
,以确保稍后可以由GC释放资源。为此,必须使用StatefulWidget.dispose
处理方法:
abstract class MyBloc {Sink foo;Sink bar;
}class MyWiget extends StatefulWidget { _MyWigetState createState() => _MyWigetState();
}class _MyWigetState extends State<MyWiget> {MyBloc bloc;void dispose() {bloc.bar.close();bloc.foo.close();super.dispose();}Widget build(BuildContext context) {// ...}
}
5. 编写关键功能的测试
依赖于手动测试的偶然情况总是存在的,拥有一组自动化的测试可以帮助您节省大量的时间和精力。由于Flutter主要针对多个平台,因此在每次更改后测试每个功能都很耗时,需要大量重复的工作。
让我们面对现实,100%的代码覆盖率用于测试总是最好的选择,然而,根据可用的时间和预算,这并不总是可能的。尽管如此,至少有测试来覆盖应用程序的关键功能仍然是必要的。
单元测试和Widget测试从一开始就是最重要的选择,与集成测试相比,它一点也不乏味。
6. 使用 raw
string
原始字符串可以用来避免只转义反斜杠和美元符合。
DON’T:
var s = 'This is demo string \ and $';
DO:
var s = r'This is demo string and $';
7. 使用相对导入而不是绝对导入
当同时使用相对导入和绝对导入时,当从两种不同的方式导入同一个类时,可能会造成混淆。为了避免这种情况,我们应该在lib/
文件夹中使用相对路径。
DON’T:
import 'package:myapp/themes/style.dart';
DO:
import '../../themes/style.dart';
8. 使用 SizedBox
代替 Container
如果有多个用例需要使用占位符。下面是一个理想的例子:
return _isNotLoaded ? Container() : YourAppropriateWidget();
Container
是一个很棒的widget,您将在Flutter中广泛使用它。Container()
扩展以适应父类给出的约束,并且不是const
构造函数。
因此,当我们必须实现占位符时,应该使用SizedBox
而不是使用Container
。
DON’T:
Widget showUI() { return Column( children: [loaded ? const ActualUI() : Container()],);
}
DO:
Widget showUI() { return Column( children: [loaded ? const ActualUI() : const SizedBox()],);
}
Better:
Widget showUI() { return Column( children: [loaded ? const ActualUI() : const SizedBox.shrink()],);
}
这样做的好处:
SizedBox
有一个const
构造函数,与Container
相比,它可以产生更高效的代码。SizedBox
是一个比要实例化的Container
更轻的对象。SizedBox.shrink()
将宽度和高度设置为0
,默认情况下初始化为null
。
您也可以使用SizedBox
而不是SizedBox.shrink
,但使用“收缩”一词可以清楚地表明此小部件将占用屏幕上最小(或零)的空间。Container
如果widget没有子对象、没有高度、没有宽度、没有连接约束和没有对齐,但父对象提供有界约束,则Container
将展开以适应父对象提供的约束。
9. 使用 log
代替 print
print()
和debugPrint()
总是用于登录控制台。如果你正在使用print()
并且你得到的输出一次太多,那么Android会时不时地丢弃一些日志行。
要避免再次遇到这种情况,请使用debugPrint()
。如果你的日志数据有足够多的数据,那么使用dart: developer log()
。这使您能够在日志输出中添加更多的粒度和信息。
DON’T:
print('data: $data');
DO:
log('data: $data');
10. 只在 Debug
模式下使用 print
确保print
和log
语句只在应用程序的 Debug
模式下使用。
可以使用kDebugMode
检测 Debug
或Release
模式
kReleaseMode
,在Release
模式中是true
kProfileMode
,在Profile
模式中是true
import "dart:developer";
import 'package:flutter/foundation.dart'; testPrint() {if (kDebugMode) {log("I am running in Debug Mode"); }
}
11. 正确的选择使用三元运算符
- 单行情况下使用三元运算符
String alert = isReturningCustomer ? 'Welcome back!' : 'Welcome, please sign up.';
- 应该使用
if
替代三元运算符的情况
Widget getText(BuildContext context) {return Row(children:[Text("Hello"),if (Platform.isAndroid) Text("Android") (这里不应该使用三元运算符)]);
}
DON’T:
Widget showUI() {return Row(children:[ const Text("Hello Flutter"),Platform.isIOS ? const Text("iPhone") : const SizedBox(), ],);
}
DO:
Widget showUI() {return Row(children:[ const Text("Hello Flutter"),if (Platform.isI0S) const Text("iPhone"), ],);
}
Also:
Widget showUI() {return Row(children:[ const Text("Hello Flutter"),if (Platform.isI0S) ...[const Text("iPhone"),const Text('MacBook'),]],);
}
12. 总是尝试使用 const Widget
当setState
调用时,如果 Widget
不会改变,我们应该将其定义为常量。它将阻止Widget
的重新构建,从而改进性能。
另外,为 Widget
使用const
构造函数可以减少垃圾收集器所需的工作。这在一开始看起来可能是一个很小的性能优化,但是当应用程序足够大或有一个视图经常被重新构建时,它实际上会产生很大的收益。const
声明也更适合热重载。
DON’T:
SizedBox(height: Dimens.space_normal)
DO:
const SizedBox(height: Dimens.space_normal)
此外,我们应该忽略不必要的const
关键字。看看下面的代码:
const Container(width: 100,child: const Text('Hello World')
);
我们不需要为Text
使用const
,因为const
已经应用于父组件了。
Dart 为 const
提供了以下Linter规则:
- prefer_const_constructors
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- Unnecessary_const
13. 总是显示的指定成员变量的类型
当成员的值类型已知时,总是突出显示成员的类型。不要在不需要的时候使用var
。由于var
是一个动态类型,需要更多的空间和时间来解析。
DON’T:
var item = 10;
final car = Car();
const timeOut = 2000;
DO:
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;
14. 需要牢记的一些要点
-
永远不要忘记将根窗口Widget包装在一个安全的区域。
-
只要有可能,请确保使用
final/const
类变量。 -
尽量不要使用不必要的注释代码。
-
尽可能创建私有变量和方法。
-
为颜色、文本样式、尺寸、常量字符串、持续时间等构建不同的类。
-
使用常量表示 API Key。
-
尽量不要在块中使用
await
关键字 -
尽量不要使用全局变量和函数。他们必须和
Class
紧密联系在一起。 -
检查Dart分析并遵循其建议
-
检查下划线,错别字建议或优化提示
-
如果该值不在代码块中使用,则使用
_
(下划线)。
DON’T:
someFuture.then((DATA_TYPE VARIABLE) => someFunc());
DO:
someFuture.then((_) => someFunc());
- 为了便于人类阅读,魔法数字应该总是有恰当的命名.
DON’T:
SvgPicture.asset(Images.frameWhite,height: 13.0,width: 13.0,
);
DO:
final _frameIconSize = 13.0;
SvgPicture.asset(Images.frameWhite,height: _frameIconSize,width: _frameIconSize,
);
15. 避免使用函数式组件
DON’T:
class HomePage extends StatelessWidget {const HomePage({Key? key}) : super(key: key);Widget build(BuildContext context) {return Scaffold(body: Padding(padding: const EdgeInsets.all(30),child: functionWidget(child: const Text('Hello')),), ); }Widget functionWidget({required Widget child}) {return Container(child: child);}
}
DO:
class HomePage extends StatelessWidget {const HomePage({Key? key}) : super(key: key);Widget build(BuildContext context) {return const Scaffold(body: Padding(padding: EdgeInsets.all(30),child: ClassWidget(child: Text('Hello')),),);}
}class ClassWidget extends StatelessWidget {final Widget child;const ClassWidget({Key? key, required this.child}) : super(key: key);Widget build(BuildContext context) {return Container(child: child);}
}
这样做的好处:
- 通过使用函数将 Widget 树拆分为多个 Widgets,您会暴露自己的bug,并错过一些性能优化。
- 使用函数不能保证您一定会有bug,但使用类可以保证您不会面临这些问题。
16. 在长列表中使用 List
Widget 的 Item Extent
属性
如果你想通过点击按钮或其他方式跳转到特定的索引,ItemExtent
可以显著提高性能。
Widget build(BuildContext context) { return Scaffold(body: ListView(controller: _scrollController,itemExtent: 600,children: List.generate(10000, (index) => Text('index: $index')),),)}
这样做的好处:
- 指定
itemExtent
比让children确定他们的范围更有效,因为滚动系统已经知道children的范围,这样可以节省时间和精力。
17. 以可读性更好的方式使用 async/await
DON’T:
Future<int> getUsersCount() async {return getUsers().then((users) {return users.length;}).catchError((e) {return 0;});}
DO:
Future<int> getUsersCount() async {try {var users = await getActiveUser();return users.length;} catch (e) {return 0;}
}
18. 使用ListView.builder
构建具有相同视图的列表
Listview.builder
创建的列表仅根据需要生成行视图。Listview.builder
将屏幕外的行视图重新用于用户可见的行视图。- 默认
ListViews
不会重用行并会一次性创建所有列表,如果列表太大,可能会立即导致性能问题。
19. 拆分 Widgets
- 将较大的
Widget
拆分为较小的Widget
组件,有助于重用和提高性能。 - 不要使用函数为更大的
Widget
返回Widget
,它可能导致对函数的不必要调用,这是昂贵的。 - 当对
State
调用setState()
时,所有派生的Widget
都将重新生成。因此,将Widget
拆分为较小的Widget
组件,可以使setState()
只调用子树中实际需要更改UI的部分。
20. 在单独的文件中使用 Colors
试着将应用程序的所有颜色都放在一个类中,如果你没有使用本地化,也可以使用字符串,这样无论何时你想添加本地化,都可以在一个地方找到所有字符串。
class AppColor {static const Color red = Color(ØxFFFF0000); static const Color green = Color(0xFF4CAF50); static const Color errorRed = Color(0xFFFF6E6E);
}
21. 使用 Dart 代码度量
Flutter代码结构的最佳实践之一是使用 Dart Code Metrics。这是提高Flutter应用程序整体质量的理想方法。
DCM(Dart Code Metrics) 是一种静态代码分析工具,可帮助开发人员监控和临时调整Flutter代码的整体质量。开发人员可以查看的各种指标包括许多参数、可执行代码行等等。
Dart Code Metrics 官方文档中提到的一些Flutter最佳实践包括:
- 避免使用
Border.all
构造函数 - 避免不必要的
setState()
- 避免返回
widgets
- 最好提取回调callback
- 每个文件最好只有一个
Widget
- 最好使用常量
const
修饰border-radius
22. 使用Fittedbox
实现Flutter响应式布局
为了在Flutter中实现响应式设计,我们可以利用FittedBox
组件。
FittedBox
是一个Flutter Widget,它限制子Widget在一定限制后的大小增长。它会根据可用的大小重新缩放子组件。
适配原理:
-
FittedBox
在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox
传递给子组件的约束为(0 <= width <= double.infinity, 0 <= height <= double.infinity
)。 -
FittedBox
对子组件布局结束后就可以获得子组件真实的大小。 -
FittedBox
知道子组件的真实大小也知道他父组件的约束,那么FittedBox
就可以通过指定的适配方式(BoxFit
枚举中指定),让子组件在FittedBox
父组件的约束范围内按照指定的方式显示。
例如,我们创建了一个容器,其中将显示用户输入的文本,如果用户输入了一个很长的文本字符串,则容器会超出其允许的大小。但是,如果我们用FittedBox
包装容器,它将根据容器的可用大小来容纳文本。如果文本超过了使用FittedBox
设置的容器大小,则会缩小文本大小以将其放入容器中。
DON’T:
Padding(padding: const EdgeInsets.symmetric(vertical: 30.0),child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)
DO:
Padding(padding: const EdgeInsets.symmetric(vertical: 30.0),child: FittedBox(child: Row(children: [Text('xx'*30)]),),
)
23. Flutter安全实践
安全是任何移动应用程序不可或缺的一部分,尤其是在这个移动优先的科技时代。为了让许多应用程序正常运行,它们需要用户的许多设备权限以及有关其财务、偏好和其他因素的敏感信息。
开发者有责任确保应用程序足够安全,以保护此类信息。Flutter提供了出色的安全保障,以下是您可以使用的最佳Flutter安全实践:
- 代码混淆
- 阻止后台快照
阻止后台快照
通常,当您的应用程序在后台运行时,它会自动在任务切换程序或多任务屏幕中显示应用程序的最后状态。当你想看看你上一次在不同应用程序上的活动是什么时,这很有用;但是,在某些情况下,您不希望在任务切换器中公开屏幕信息。例如,你不希望你的银行账户详细信息在后台显示在应用程序的屏幕上。您可以使用secure_application 包来保护您的Flutter应用程序免受此类问题的影响。
24. 其他 Tips
-
1)
Row
和Column
布局设置主轴对齐方式为spaceEvenly
会将空余空间在每个图像之间、之前和之后均匀地划分:mainAxisAlignment: MainAxisAlignment.spaceEvenly
实际中,这对于行或者列中的子控件间距均匀分布十分有用。 -
2)将
Row
和Column
的mainAxisSize
设置为MainAxisSize.min
,可以使将子项紧密组合在一起,默认情况下,行或列沿其主轴会占用尽可能多的空间。 -
3)
Expanded
或者Wrap
组件可以解决界面 overflow 的问题,Expanded
可以设置flex
占比 -
4)每个
Element
都对应一个RenderObject
,我们可以通过Element.renderObject
来获取。RenderObject
的主要职责是Layout和绘制,所有的RenderObject
会组成一棵渲染树 Render Tree。 -
5)
RenderObject
就是渲染树中的一个对象,它拥有一个parent
和一个parentData
插槽(slot) 这个插槽是一个预留变量,主要用来存储child
的偏移量数据offset
(当然还有其他的),这个偏移量在绘制阶段会用到。 -
6)根据
layout()
源码可以看出只有sizedByParent
为true
时,performResize()
才会被调用,而performLayout()
是每次布局都会被调用的。sizedByParent
意为该节点的大小是否仅通过parent
传给它的constraints
就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满parent
的大小,那么sizedByParent
就应该返回true
,此时其大小在performResize()
中就确定了,在后面的performLayout()
方法中将不会再被修改了,这种情况下performLayout()
只负责布局子节点。 -
7)布局layout过程 最终的调用栈将会变成:layout() > performResize() / performLayout() > child.layout() > … ,如此递归完成整个UI的布局。
-
8)绘制过程 会遍历其子节点,然后调用paintChild()来绘制子节点,同时将子节点
ParentData
中在layout阶段保存的offset
加上自身偏移作为第二个参数传递给paintChild()
,而如果子节点还有子节点时,paintChild()
方法还会调用子节点的paint()
方法,如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() … 。 -
9)isRepaintBoundary可以提高绘制性能 当有
RenderObject
绘制的很频繁或很复杂时,可以通过RepaintBoundary
Widget来指定isRepaintBoundary
为true
,这样在绘制时仅会重绘自身而无需重绘它的parent
,如此便可提高性能。 -
10)Flutter 中的 widget 由在其底层的 RenderBox 对象渲染而成。渲染框由其父级 widget 给出约束,并根据这些约束调整自身尺寸大小。 约束是由最小宽度、最大宽度、最小高度、最大高度四个方面构成;尺寸大小则由特定的宽度和高度两个方面构成。
-
11)一般来说,从如何处理约束的角度来看,有以下三种类型的渲染框:
- 尽可能大。比如
Center
和ListView
的渲染框。 - 与子 widget 一样大,比如
Transform
和Opacity
的渲染框。 - 特定大小,比如
Image
和Text
的渲染框。
当传递无边界(最大宽度或最大高度为double.INFINITY)约束给类型为尽可能大的框时会失效,在 debug 模式下,则会抛出异常。
渲染框具有无边界约束的最常见情况是:当其被置于 flex boxes (Row 和 Column) 内以及可滚动区域(ListView 和其它 ScrollView 的子类)内时。
- 尽可能大。比如
-
12)Flex 本身(Row 和 Column) 的行为会有所不同,这取决于其在给定方向上是处于有边界约束还是无边界约束。
-
在有边界约束条件下,它们在给定方向上会尽可能大。
-
在无边界约束条件下,它们试图让其子 widget 自适应这个给定的方向。在这种情况下,不能将子
widget
的flex
属性设置为0
(默认值)以外的任何值。这意味着在 widget 库中,当一个flex
框嵌套在另外一个flex
框或者嵌套在可滚动区域内时,不能使用Expanded
。如果这样做了,就会收到异常。
在 交叉 方向上,如
Column
(垂直的flex
)的宽度和Row
(水平的flex
)的高度,它们必将不能是无界的,否则它们将无法合理地对齐它们的子widget
。 -
-
13)
Text
设置softwrap
为true
,文本将在填充满列宽后在单词边界处自动换行。 -
14)Flutter更喜欢组合而不是继承。组合定义“
has a
”关系,继承定义“is a
”关系。 -
15)widget在刷新中应该是不可变的,但是状态对象
State
是可变的。 -
16)一个
StatefullWidget
通过一个关联的状态对象跟踪它自己的内部状态。StatefullWidget
是“哑的”,当它从widget
树中删除时,它会被完全销毁。 -
17)在Flutter中,
widget
由其关联的RenderBox
对象进行渲染。这些render box负责告诉widget其实际的物理大小。这些对象从它们的父对象那里接收约束,然后使用这些约束来确定它们的实际大小。 -
18)
Container
组件是一个“方便”的widget
,它提供了大量的属性,否则您可能需要从各个Widget
中获得这些属性。