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
},
);
}
}
Flutter布局有一个经验法则,每个Flutter应用程序开发人员都需要知道: 约束向下,大小向上,父元素设置位置。
widget有来自其父组件的约束。已知约束是一组包含四个double的集合: 最小和最大宽度,最小和最大高度。
接下来,widget将遍历它自己的子列表。widget一个接一个地命令其子widget的约束条件是什么(每个子widget的约束条件可能不同),然后询问每个子widget想要的大小。
接下来,widget依次定位它的子widget(水平x轴,垂直y轴)。然后,widget将自己的大小通知其父组件(当然,在原始约束范围内)。
在Flutter中,所有widget都基于它们的父组件或它们的框约束来提供自身。 widget的大小必须在其父组件设置的约束范围内。
如果我们要对同一个对象执行一系列操作,那么我们应该选择 ..
运算符:
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
检查。
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>();
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) {
// ...
}
}
依赖于手动测试的偶然情况总是存在的,拥有一组自动化的测试可以帮助您节省大量的时间和精力。由于Flutter主要针对多个平台,因此在每次更改后测试每个功能都很耗时,需要大量重复的工作。
让我们面对现实,100%的代码覆盖率用于测试总是最好的选择,然而,根据可用的时间和预算,这并不总是可能的。尽管如此,至少有测试来覆盖应用程序的关键功能仍然是必要的。
单元测试和Widget测试从一开始就是最重要的选择,与集成测试相比,它一点也不乏味。
raw
string原始字符串可以用来避免只转义反斜杠和美元符合。
DON’T:
var s = 'This is demo string \ and $';
DO:
var s = r'This is demo string and $';
当同时使用相对导入和绝对导入时,当从两种不同的方式导入同一个类时,可能会造成混淆。为了避免这种情况,我们应该在lib/
文件夹中使用相对路径。
DON’T:
import 'package:myapp/themes/style.dart';
DO:
import '../../themes/style.dart';
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
将展开以适应父对象提供的约束。log
代替 print
print()
和debugPrint()
总是用于登录控制台。如果你正在使用print()
并且你得到的输出一次太多,那么Android会时不时地丢弃一些日志行。
要避免再次遇到这种情况,请使用debugPrint()
。如果你的日志数据有足够多的数据,那么使用dart: developer log()
。这使您能够在日志输出中添加更多的粒度和信息。
DON’T:
print('data: $data');
DO:
log('data: $data');
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");
}
}
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'),
]
],
);
}
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规则:
当成员的值类型已知时,总是突出显示成员的类型。不要在不需要的时候使用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;
永远不要忘记将根窗口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,
);
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);
}
}
这样做的好处:
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的范围,这样可以节省时间和精力。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;
}
}
ListView.builder
构建具有相同视图的列表Listview.builder
创建的列表仅根据需要生成行视图。Listview.builder
将屏幕外的行视图重新用于用户可见的行视图。ListViews
不会重用行并会一次性创建所有列表,如果列表太大,可能会立即导致性能问题。Widget
拆分为较小的Widget
组件,有助于重用和提高性能。Widget
返回Widget
,它可能导致对函数的不必要调用,这是昂贵的。State
调用setState()
时,所有派生的Widget
都将重新生成。因此,将Widget
拆分为较小的Widget
组件,可以使 setState()
只调用子树中实际需要更改UI的部分。Colors
试着将应用程序的所有颜色都放在一个类中,如果你没有使用本地化,也可以使用字符串,这样无论何时你想添加本地化,都可以在一个地方找到所有字符串。
class AppColor {
static const Color red = Color(ØxFFFF0000);
static const Color green = Color(0xFF4CAF50);
static const Color errorRed = Color(0xFFFF6E6E);
}
Flutter代码结构的最佳实践之一是使用 Dart Code Metrics。这是提高Flutter应用程序整体质量的理想方法。
DCM(Dart Code Metrics) 是一种静态代码分析工具,可帮助开发人员监控和临时调整Flutter代码的整体质量。开发人员可以查看的各种指标包括许多参数、可执行代码行等等。
Dart Code Metrics 官方文档中提到的一些Flutter最佳实践包括:
Border.all
构造函数setState()
widgets
Widget
const
修饰border-radius
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)]),
),
)
安全是任何移动应用程序不可或缺的一部分,尤其是在这个移动优先的科技时代。为了让许多应用程序正常运行,它们需要用户的许多设备权限以及有关其财务、偏好和其他因素的敏感信息。
开发者有责任确保应用程序足够安全,以保护此类信息。Flutter提供了出色的安全保障,以下是您可以使用的最佳Flutter安全实践:
阻止后台快照
通常,当您的应用程序在后台运行时,它会自动在任务切换程序或多任务屏幕中显示应用程序的最后状态。当你想看看你上一次在不同应用程序上的活动是什么时,这很有用;但是,在某些情况下,您不希望在任务切换器中公开屏幕信息。例如,你不希望你的银行账户详细信息在后台显示在应用程序的屏幕上。您可以使用secure_application 包来保护您的Flutter应用程序免受此类问题的影响。
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
的渲染框。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
中获得这些属性。