Flutter 笔记 | Best Practice Tips for Flutter

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

确保printlog语句只在应用程序的 Debug 模式下使用。

可以使用kDebugMode检测 DebugRelease模式

  • 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在一定限制后的大小增长。它会根据可用的大小重新缩放子组件。

适配原理:

  1. FittedBox 在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox 传递给子组件的约束为(0 <= width <= double.infinity, 0 <= height <= double.infinity)。

  2. FittedBox 对子组件布局结束后就可以获得子组件真实的大小

  3. 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安全实践:

  1. 代码混淆
  2. 阻止后台快照

阻止后台快照

通常,当您的应用程序在后台运行时,它会自动在任务切换程序或多任务屏幕中显示应用程序的最后状态。当你想看看你上一次在不同应用程序上的活动是什么时,这很有用;但是,在某些情况下,您不希望在任务切换器中公开屏幕信息。例如,你不希望你的银行账户详细信息在后台显示在应用程序的屏幕上。您可以使用secure_application 包来保护您的Flutter应用程序免受此类问题的影响。

24. 其他 Tips

  • 1)RowColumn布局设置主轴对齐方式为 spaceEvenly 会将空余空间在每个图像之间、之前和之后均匀地划分: mainAxisAlignment: MainAxisAlignment.spaceEvenly 实际中,这对于行或者列中的子控件间距均匀分布十分有用。

  • 2)将RowColumnmainAxisSize 设置为 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() 源码可以看出只有 sizedByParenttrue 时,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来指定isRepaintBoundarytrue,这样在绘制时仅会重绘自身而无需重绘它的 parent,如此便可提高性能。

  • 10)Flutter 中的 widget 由在其底层的 RenderBox 对象渲染而成。渲染框由其父级 widget 给出约束,并根据这些约束调整自身尺寸大小。 约束是由最小宽度、最大宽度、最小高度、最大高度四个方面构成;尺寸大小则由特定的宽度和高度两个方面构成。

  • 11)一般来说,从如何处理约束的角度来看,有以下三种类型的渲染框:

    • 尽可能大。比如 CenterListView 的渲染框。
    • 与子 widget 一样大,比如 TransformOpacity 的渲染框。
    • 特定大小,比如 ImageText 的渲染框。

    当传递无边界(最大宽度或最大高度为double.INFINITY)约束给类型为尽可能大的框时会失效,在 debug 模式下,则会抛出异常。

    渲染框具有无边界约束的最常见情况是:当其被置于 flex boxes (Row 和 Column) 内以及可滚动区域(ListView 和其它 ScrollView 的子类)内时

  • 12)Flex 本身(Row 和 Column) 的行为会有所不同,这取决于其在给定方向上是处于有边界约束还是无边界约束。

    • 在有边界约束条件下,它们在给定方向上会尽可能大。

    • 在无边界约束条件下,它们试图让其子 widget 自适应这个给定的方向。在这种情况下,不能将子 widgetflex属性设置为 0(默认值)以外的任何值。这意味着在 widget 库中,当一个 flex 框嵌套在另外一个 flex 框或者嵌套在可滚动区域内时,不能使用 Expanded。如果这样做了,就会收到异常。

    交叉 方向上,如 Column(垂直的 flex)的宽度和 Row(水平的 flex)的高度,它们必将不能是无界的,否则它们将无法合理地对齐它们的子 widget

  • 13)Text 设置 softwraptrue,文本将在填充满列宽后在单词边界处自动换行。

  • 14)Flutter更喜欢组合而不是继承。组合定义“has a”关系,继承定义“is a”关系。

  • 15)widget在刷新中应该是不可变的,但是状态对象State是可变的。

  • 16)一个StatefullWidget通过一个关联的状态对象跟踪它自己的内部状态。StatefullWidget是“哑的”,当它从widget树中删除时,它会被完全销毁。

  • 17)在Flutter中,widget由其关联的RenderBox对象进行渲染。这些render box负责告诉widget其实际的物理大小。这些对象从它们的父对象那里接收约束,然后使用这些约束来确定它们的实际大小。

  • 18)Container组件是一个“方便”的widget,它提供了大量的属性,否则您可能需要从各个Widget中获得这些属性。

你可能感兴趣的:(Flutter,flutter,Flutter最佳实践,Flutter开发规范,Flutter建议指南)