Flutter 最佳实践

3,086 阅读4分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

开发人员不管使用哪种编程语言,都需要在提高代码可读性、可维护性、健壮性等方面努力。Flutter 同样需要一些最佳实践,让我们也能编写一致、稳健、快速的代码,同时也方便我们阅读和查看代码。Effective Dart提供了详细的指南,但是相信大家很难耐心的看完并能记住。本文结合长期开发以来的经验,列举几个最常见的示例。

命名规则

  • UpperCamelCase 大驼峰命名法,每个单词首字母大写。
  • lowerCamelCase 小驼峰命名法,第一个单词首字母小写,其他单词首字母大写。
  • lowercase_with_underscores 小写下划线命名法,单词全部小写,单词间用下划线_连接。
  1. 变量、常量、方法名、参数使用小驼峰命名,特别注意常量也是小驼峰命名:

    String name;
    int phoneNumber = 123456789;
    const songPrice = 9.99;
    void sum(int songPrice){
      //....
    }
    
  2. 类名、构造函数名、扩展类名、定义类型、枚举名使用大驼峰命名:

    extension MyFancyList<T> on List<T> { ... }
    class Foo {
      const Foo([Object? arg]);
    }
    typedef Predicate<T> = bool Function(T value);
    enum Status {
       none,
       running,
       stopped,
       paused
    }
    
  3. Packages 、 Libraries 、文件名、导包别名使用小写下划线命名:

    library peg_parser.source_scanner;
    import 'dart:math' as math;
    import 'package:angular_components/angular_components'
        as angular_components;
        
    

导包使用相对路径

一个项目同时使用相对路径和绝对路径,那么看着导包就比较混乱,为了避免这种情况,应该在文件夹中使用相对路径。因为 ide 还没有那么智能,更改文件夹名称或者移动文件夹,导包路径会出错。

// Don't
import 'package:demo/src/utils/dialog_utils.dart';


// Do
import '../../../utils/dialog_utils.dart';

为变量指定类型,可空类型不用初始化为空

Dart 支持类型推断,但是为了更好的阅读性,一目了然的知道类型,我们需要指定类型:

//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;

而对于可空的类型,不需要显示初始化为空,因为默认就是空值:

// Do
String name;

// Don't
String name = null;

使用操作符和级联操作符

使用??而不是判空表达式进行空检查,缩短代码:

// Do
a = x ?? y;
// Don't
a = x == null ?  y : x;

// Do
a = x?.y;
// Don't
a = x == null ? null : x.y;

// Do
List<String>? strs;
if (strs?.isNotEmpty ?? false) {
//...
}
// Don't
List<String>? strs;
if (strs!=null&&strs.isNotEmpty) {
//...
}

使用展开操作符展开列表项:

//Do
var y = [4,5,6];
var x = [1,2,...y];

//Don't
var y = [4,5,6];
var x = [1,2];
x.addAll(y);

如果在同一个对象上执行一系列操作,应该使用级联表达式:

// Do
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close(); 

// Don't
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();  

使用 lambda 表达式:

//Do
get width => right - left;
Widget getProgressBar() => CircularProgressIndicator(
      valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
    );
    
    
//Don't
get width {
  return right - left;
}
Widget getProgressBar() {
  return CircularProgressIndicator(
    valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
  );
}

//-------------------------------------------------------------

List<String> names=[]

// Do
names.forEach(print);

// Don’t
names.forEach((name) {
  print(name);
});

使用原始字符串,可以避免转义反斜线和美元符号:

//Do
var s = r'This is demo string \ and $';

//Don't
var s = 'This is demo string \\ and \$';

使用插值构建字符串,而不是使用+来拼接字符串,可以使字符串更清洁、更短。

// Do
var description = 'Hello, $name! You are ${year - birth} years old.';

//Don’t
var description = 'Hello, ' + name + '! You are ' + (year - birth).toString() + ' years old.';

使用操作符is,避免使用强转,如果类型不一致,强转可能会抛出异常:

//Do
if (item is Animal){
 item.name = 'Lion';
} 
  
//Don't
(item as Animal).name = 'Lion';

避免使用print()

打印日志太多的时候,Android 会丢弃一些,并且 print()会造成日志泄露,所以使用debugPrint()dart:developer:log()

使用async/await处理异步任务

异步代码很难读取和调试。async``await语法提高了可读性,避免了地狱回调和嵌套,使异步代码变得和同步代码一样:

// Do
Future<int> countActiveUser() async {
  try {
    var users = await getActiveUser();
     
    return users?.length ?? 0;
  
  } catch (e) {
    log.error(e);
    return 0;
  }
}

// Don’t
Future<int> countActiveUser() {
  return getActiveUser().then((users) {
    
    return users?.length ?? 0;

  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

使用 Const 和抽取嵌套控件

如果在调用setState时不需要改变状态的控件,需要声明为 Const 类型,避免重建:

Container(
      padding: const EdgeInsets.only(top: 10),
      color: Colors.black,
      child: const Center(
        child: const Text(
          "No Data found",
          style: const TextStyle(fontSize: 30, fontWeight: FontWeight.w800),
        ),
      ),
    );

如果嵌套控件过多,抽离为单独的小部件,不要抽离为方法:

抽成方法会重建,具体情况从渲染流程解说Flutter老鸟也常犯的错误——多次重建

// Do
class _NonsenseWidget extends StatelessWidget {
  const _NonsenseWidget();
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Text('我是第一行'),
        Text('我是第二行'),
        Text('我是第三行'),
      ],
    );
  }
}

// Don’t
Row _buildRow() {
    return Row(
            children: <Widget>[
              Text('我是第一行'),
              Text('我是第二行'),
              Text('我是第三行'),
            ],
          );
  }