Flutter 最佳实践和编码准则

Flutter 最佳实践和编码准则

视频

前言

最佳实践是一套既定的准则,可以提高代码质量、可读性和可靠性。它们确保遵循行业标准,鼓励一致性,并促进开发人员之间的合作。通过遵循最佳实践,代码变得更容易理解、修改和调试,从而提高整体软件质量。

原文 https://ducafecat.com/blog/flutter-best-practices-and-coding-guidelines

参考

https://dart.dev/effective-dart/style

正文开始

有许多准则和实践可以采用来提高代码质量和应用性能。

Naming convention 命名规范

  • 类、枚举、类型定义、混入和扩展的名称应使用大驼峰命名法。

    Good

    class ClassName {}

    extension ExtensionName on String {}

    enum EnumName {}

    mixin MixinName{}

    typedef FunctionName = void Function();

    Bad

    class Classname {
    }

    extension Extensionname on String {
    }

    enum Enumname {
    }
    mixin Mixinname{}

    typedef Functionname = void Function();

  • Libraries、包、目录和源文件的名称应该使用蛇形命名法(小写字母加下划线)。

    Good

    my_package
    └─ lib
    └─ bottom_nav.dart

    Bad

    mypackage
    └─ lib
    └─ bottom-nav.dart

  • 导入的前缀命名应该使用蛇形命名法(小写字母加下划线)。

    Good

    import 'package:dio/dio.dart' as dio;

    #Bad
    import 'package:dio/dio.dart' as Dio;

  • 变量、常量、参数和命名参数应该使用小驼峰命名法。

    Good

    int phoneNumber;
    const pieValue=3.14;

    // parametrs
    double calculateBMI(int weightInKg, int heightInMeter) {
    return weightInKg / (heightInMeter * heightInMeter);
    }

    //named parametrs
    double calculateBMI({int? weightInKg, int? heightInMeter}) {
    if(weightInKg !=null && heightInMeter !=null){
    return weightInKg / (heightInMeter * heightInMeter);
    }
    }

    Bad

    int phone_number;
    const pie_value=3.14;

    // parametrs
    double calculateBMI(int weight_in_kg, int height_in_meter) {
    return weight_in_kg / (height_in_meter * height_in_meter);
    }

    //named parametrs
    double calculateBMI({int? weight_in_kg, int? height_in_meter}) {
    return weight_in_kg / (height_in_meter * height_in_meter);
    }

  • 应该遵循适当有意义的命名规范。

    Good

    Color backgroundColor;
    int calculateAge(Date dob);

    Bad

    Color bg;
    int age(Date date);

  • 私有变量名前面加下划线。

    class ClassName {

    // private variable
    String _variableName;
    }

使用可空运算符

在处理条件表达式时,建议使用 ?? (如果为null)和 ?. (null aware)运算符,而不是显式的null检查。 ?? (如果为空)运算符:

# Bad
String? name;
name= name==null ? "unknown": name;

# Good
String? name;
name= name ?? "unknown";

?. (空值安全)运算符:

# Bad
String? name;
name= name==null? null: name.length.toString();

# Good
String? name;
name=name?.length.toString();

为了避免潜在的异常情况,在Flutter中建议使用 is 运算符而不是 as 强制转换运算符。 is 运算符允许更安全地进行类型检查,如果转换不可能,也不会抛出异常。

# Bad
(person as Person).name="Ashish";

# Good 
if(person is Person){
  person.name="Ashish";
}

避免不必要地创建lambda函数

Lambda 函数(也称为匿名函数或闭包)是一种无需声明函数名称即可定义的函数。它是一种简洁、灵活的函数编写方式,通常用于需要传递函数作为参数或以函数作为返回值的语言特性中。

在 Dart 和许多其他编程语言中,Lambda 函数可以使用箭头语法或 () {} 语法来定义。例如,在 Dart 中,下面的代码演示了如何使用箭头语法定义一个 lambda 函数:在可以使用 tear-off 的情况下,避免不必要地创建 lambda 函数。如果一个函数只是简单地调用一个带有相同参数的方法,就没有必要手动将调用包装在 lambda 函数中。

# Bad
void main(){
  List<int> oddNumber=[1,3,4,5,6,7,9,11];
  oddNumber.forEach((number){
   print(number);
  });
}

# Good 
void main(){
  List<int> oddNumber=[1,3,4,5,6,7,9,11];
  oddNumber.forEach(print);
}

使用扩展集合简化您的代码

  • 当你已经在另一个集合中存储了现有的项目时,利用扩展集合可以简化代码。

    Bad

    List<int> firstFiveOddNumber=[1,3,5,7,9];
    List<int> secondFiveOddNumber=[11,13,15,17,19];
    firstFiveOddNumber.addAll(secondFiveOddNumber);
    

    Good

    List<int> secondFiveOddNumber=[11,13,15,17,19];
    List<int> firstFiveOddNumber=[1,3,5,7,9,...secondFiveOddNumber];
    

使用级联操作简化对象操作

  • Cascades(级联)操作符非常适合在同一对象上执行一系列操作,使代码更加简洁易读。

    class Person {
    String? name;
    int? age;
    Person({
    this.name,
    this.age,
    });

    @override
    String toString() {
      return "name: $name age $age";
    }
    

    }

    Bad

    void main(){
    final person=Person();
    person.name="Ashish";
    person.age=25;
    print(person.toString());
    }

    Good

    void main(){
    final person=Person();
    person
    ..name="Ashish"
    ..age=25;
    print(person.toString());
    }

使用if条件在行和列中实现最佳widget 渲染

  • 在根据行或列中的条件渲染widget 时,建议使用if条件而不是可能返回null的条件表达式。

    Bad

    Column(
    children: [
    isLoggedIn
    ? ElevatedButton(
    onPressed: () {},
    child: const Text("Go to Login page"),
    )
    : const SizedBox(),
    ],
    ),

    Good

    Column(
    children: [
    if(isLoggedIn)
    ElevatedButton(
    onPressed: () {},
    child: const Text("Go to Login page"),
    )
    ],
    ),

使用箭头函数

  • 如果一个函数只有一条语句,使用 () => 箭头函数。

    Bad

    double calculateBMI(int weight_in_kg, int height_in_meter) {
    return weight_in_kg / (height_in_meter * height_in_meter);
    }

    Good

    double calculateBMI(int weight_in_kg, int height_in_meter) =>
    weight_in_kg / (height_in_meter * height_in_meter);

删除任何打印语句、未使用的和被注释的代码

在 Flutter 中,使用 print 语句来输出调试信息是可行的,但不建议在生产环境中使用,因为它有几个缺点:

  1. 输出的信息可能难以区分:在 Flutter 应用程序中,输出的信息可能会与应用程序本身的输出混杂在一起,这可能会导致输出的信息难以区分。
  2. 输出的信息可能不可靠: print 语句输出的信息通常会被缓存,因此可能不会立即显示出来。这可能会导致在应用程序崩溃之前,无法看到最后一次输出的信息。
  3. 输出的信息可能会影响应用程序性能:在某些情况下,输出的信息可能会大量占用应用程序的资源,影响应用程序的性能。
    因此,Flutter 推荐使用专门的日志记录库,如 loggerflutter_bloc 中的 BlocObserver,以便在应用程序中输出可靠、易于区分和可控制的日志。这些库允许您定义输出的日志级别、输出到不同的目标(如控制台或文件)以及格式化日志消息等。例如,使用 logger 库,您可以按以下方式输出日志消息:
# Bad 
# production mode

// commented message---main method   
void main(){
 print("print statement"); 
 //..rest of code
}
void unusedFunction(){
}

# Good 
# production mode
  
void main(){
//..rest of code
}

正确的文件夹结构

  • 将代码分离到适当的文件夹结构中,包括提供者(providers)、模型(models)、屏幕/页面(screens/pages)、服务(services)、常量(constants)和工具(utils)。

    project/
    lib/
    providers/
    auth_provider.dart
    models/
    user.dart
    screens/
    home_screen.dart
    login_screen.dart
    utils.dart
    constants.dart
    services.dart
    main.dart

  • 代码格式正确,适当使用 lints 配置。

    include: package:flutter_lints/flutter.yaml
    analyzer:
    errors:
    require_trailing_commas: error
    linter:
    rules:
    require_trailing_commas: true
    prefer_relative_imports: true

  • 尝试通过在 utils 文件夹中保存的辅助函数中实现代码的可重用性。

    utils.dart

    import 'package:intl/intl.dart';

    String formatDateTime(DateTime dateTime) {
    final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
    return formatter.format(dateTime);
    }

  • widget 还应该被设计成可重复使用的,并可以单独保存在widgets文件夹中。

    text_input.dart

    import 'package:flutter/material.dart';

    class TextInput extends StatelessWidget {
    final String? label;
    final String? hintText;
    final TextEditingController? controller;
    final TextInputType keyboardType;
    final bool obscureText;
    final String? Function(String?)? validator;
    final Widget? suffix;

    const TextInput({
      this.label,
      this.hintText,
      this.suffix,
      this.controller,
      this.validator,
      this.obscureText = false,
      this.keyboardType = TextInputType.text,
    });
    
    @override
    Widget build(BuildContext context) {
      return TextFormField(
        decoration: InputDecoration(
          labelText: label,
          hintText:hintText
          suffixIcon:suffix,
        ),
        controller: controller,
        obscureText: obscureText,
        validator:validator
        keyboardType: keyboardType,
      );
    }
    

    }

  • 在UI界面中避免使用静态或硬编码的字符串,建议根据其范围将其组织在单独的文件夹或文件中。

    Good

    validators/

    common_validator.dart

    mixin CommonValidator{
    String? emptyValidator(String value) {
    if (value.isEmpty) {
    return 'Please enter';
    } else {
    return null;
    }
    }
    }

    #config/themes
    colors.dart

    class AppColors{
    static const white=Color(0xffffffff);
    static const black=Color(0xff000000);
    }

    class LoginPage extends StatelessWidget with CommonValidator {
    const LoginPage({super.key});

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          backgroundColor: AppColors.black, // good
          title: const Text("Login page"),
        ),
        body: Column(
          children: [
            TextInput(
              label: "email",
              hintText: "email address",
              validator: emptyValidator,  // good 
            )
          ],
        ),
      );
    }
    

    }

    #Bad
    class LoginPage extends StatelessWidget {
    const LoginPage({super.key});

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          backgroundColor: const Color(0xff000000), // bad
          title: const Text("Login page"),
        ),
        body: Column(
          children: [
            TextInput(
              label: "email",
              hintText: "email address",
              validator: (value) {   // bad
                if (value!.isEmpty) {
                  return 'Please enter';
                } else {
                  return null;
                }
              },
            )
          ],
        ),
      );
    }
    

    }

widget 组织

  • 将widget 拆分为不同的widget ,而不是同一个文件。

  • 在widget 中使用const

  • 当在一个State上调用setState()时,所有子孙widget都会重新构建。因此,将widget拆分为小的widget,这样setState()调用只会重新构建那些实际需要改变UI的子树的部分。

    Bad

    class LoginPage extends StatefulWidget {
    const LoginPage({super.key});

    @override
    State<LoginPage> createState() => _LoginPageState();
    

    }

    class _LoginPageState extends State<LoginPage> {
    bool _secureText = true;
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: const Text("Login page"),
    ),
    body: Column(
    children: [
    const TextInput(
    label: "Email",
    hintText: "Email address",
    ),
    TextInput(
    label: "Password",
    hintText: "Password",
    obscureText: _secureText,
    suffix: IconButton(
    onPressed: () {
    setState(() {
    _secureText = !_secureText;
    });
    },
    icon: Icon(
    _secureText ?
    Icons.visibility_off
    : Icons.visibility)),
    ),
    ElevatedButton(
    onPressed: () {},
    child: const Text("Login"))
    ],
    ),
    );
    }
    }

    Good

    class LoginPage extends StatelessWidget {
    const LoginPage({super.key});

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text("Login page"),
        ),
        body: Column(
          children: [
            const TextInput(
              label: "Email",
              hintText: "Email address",
            ),
            const TextInput(
              label: "Password",
              hintText: "Password",
              obscureText: true,
            ),
            ElevatedButton(
            onPressed: () {}, 
            child: const Text("Login"))
          ],
        ),
      );
    }
    

    }

    //separate TextFormField Component

    class TextInput extends StatefulWidget {
    final String? label;
    final TextEditingController? controller;
    final String? hintText;
    final TextInputType keyboardType;
    final String? Function(String?)? validator;
    final bool obscureText;

    const TextInput({
      super.key,
      this.label,
      this.hintText,
      this.validator,
      this.obscureText = false,
      this.controller,
      this.keyboardType = TextInputType.text,
    });
    
    @override
    State<TextInput> createState() => _TextInputState();
    

    }

    class _TextInputState extends State<TextInput> {
    bool _secureText = false;
    @override
    void initState() {
    _secureText = widget.obscureText;
    super.initState();
    }

    @override
    Widget build(BuildContext context) {
      return TextFormField(
        decoration: InputDecoration(
            labelText: widget.label,
            hintText: widget.hintText,
            suffixIcon: widget.obscureText
                ? IconButton(
                    onPressed: () {
                      setState(() {
                        _secureText = !_secureText;
                      });
                    },
                    icon: Icon(
                      _secureText ? Icons.visibility_off : Icons.visibility,
                      color: Colors.grey,
                    ),
                  )
                : null),
        controller: widget.controller,
        validator: widget.validator,
        obscureText: _secureText,
        keyboardType: widget.keyboardType,
      );
    }
    

    }

遵循代码规范

  • 在lib/目录中,避免使用相对导入。请使用包导入。

  • 避免使用 print 打印语句

    Bad

    import 'widgets/text_input.dart';

    import 'widgets/button.dart'

    import '../widgets/custom_tile.dart';

    Good

    import 'package:coding_guidelines/widgets/text_input.dart';

    import 'package:coding_guidelines/widgets/button.dart'

    import 'package:coding_guidelines/widgets/custom_tile.dart';

    Bad

    void f(int x) {
    print('debug: $x');
    ...
    }

    Good

    void f(int x) {
    debugPrint('debug: $x');
    }

    linter:
    rules:
    - avoid_empty_else
    - always_use_package_imports
    - avoid_print

适当的状态管理

  • 使用Provider作为推荐的状态管理包,但是Riverpod与Provider相似,可以被视为其改进版本。

  • 您还可以选择使用其他状态管理方法,如Bloc、Riverpod、Getx和Redux。

  • 业务逻辑应该与用户界面分离。

    Bad

    class CounterScreen extends StatefulWidget {
    const CounterScreen({
    super.key,
    });
    @override
    State<CounterScreen> createState() => _CounterScreenState();
    }

    class _CounterScreenState extends State<CounterScreen> {
    int _counter = 0;

    void _incrementCounter() {
      setState(() {
        _counter++;
      });
    }
    
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text("Counter APP"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      );
    }
    

    }

    Good

    // separte logic from UI
    // provider state management
    class CounterProvider with ChangeNotifier {
    int _counter = 0;

    int get counter => _counter;
    
    void incrementCounter() {
      _counter++;
      notifyListeners();
    }
    
    void decrementCounter() {
      _counter--;
      notifyListeners();
    }
    

    }

    // UI

    class CounterScreen extends StatelessWidget {
    const CounterScreen({
    super.key,
    });

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text("Counter APP"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Consumer<CounterProvider>(
                builder: (context, counter, child) {
                  return Text(
                    counter.counter.toString(),
                    style: Theme.of(context).textTheme.headlineMedium,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<CounterProvider>().incrementCounter(),
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      );
    }
    

    }

升级第三方包

  • 在应用程序中使用的任何第三方包都需要进行验证,因为有时它可能会破坏构建或与当前的Flutter版本不同步。特别是在升级Flutter时,务必在升级后检查所有插件和第三方包。请确保它们与当前版本兼容。

错误处理和日志记录

  • 使用try-catch块来正确处理代码中的异常和错误。

  • 使用像 pretty_dio_loggerdio_logger 这样的日志记录库来记录重要事件或错误。

    Good

    final dio = Dio()
    ..interceptors.add(PrettyDioLogger(
    requestHeader: true,
    requestBody: true,
    responseBody: true,
    responseHeader: false,
    compact: false,
    ));

    Future<dynamic> fetchNetworkData() async{
    try {
    // Simulating an asynchronous network call
    final data= await dio.get('endpoint');
    return data;
    } catch (e, stackTrace) {
    print('An exception occurred: $e');
    print('Stack trace: $stackTrace');
    return e;
    // Perform additional error handling actions
    }
    }

    Bad

    final dio = Dio();

    Future<dynamic> fetchNetworkData() {
    dio.get('endpoint').then((data){
    return data;
    )}.catchError((e) {
    log.error(e);
    return e;
    });
    }

Testing 测试

  • 编写单元测试和widget 测试来确保代码的正确性。

  • 使用像 flutter_test 这样的测试框架来编写和运行测试。

  • 追求高代码覆盖率,尤其是对于应用程序的关键部分。

    Good

    // counter app integartion testing
    void main() {
    IntegrationTestWidgetsFlutterBinding.ensureInitialized();

    group('end-to-end test', () {
      testWidgets('tap on the floating action button, verify counter',
          (tester) async {
        app.main();
        await tester.pumpAndSettle();
    
        // Verify the counter starts at 0.
        expect(find.text('0'), findsOneWidget);
    
        // Finds the floating action button to tap on.
        final Finder fab = find.byTooltip('Increment');
    
        // Emulate a tap on the floating action button.
        await tester.tap(fab);
    
        // Trigger a frame.
        await tester.pumpAndSettle();
    
        // Verify the counter increments by 1.
        expect(find.text('1'), findsOneWidget);
      });
    });
    

    }

版本控制和协作

  • 使用像Git这样的版本控制系统来跟踪变更并与其他开发者合作。

  • 遵循Git的最佳实践,例如创建有意义的提交信息和分支策略。

    The commit type can include the following:

    feat -- a new feature is introduced with the changes
    fix -- a bug fix has occurred
    chore -- changes that do not relate to a fix or feature and don't modify src or test files (for example updating dependencies)
    refactor -- refactored code that neither fixes a bug nor adds a feature
    docs -- updates to documentation such as a the README or other markdown files
    style -- changes that do not affect the meaning of the code, likely related to code formatting such as white-space, missing semi-colons, and so on.
    test -- including new or correcting previous tests
    perf -- performance improvements
    ci -- continuous integration related
    build -- changes that affect the build system or external dependencies
    revert -- reverts a previous commit

    Good

    feat: button component
    chore: change login translation

    Bad

    fixed bug on login page
    Changed button style
    empty commit messages

持续集成与交付

  • 建立一个持续集成(CI)流水线,自动运行测试和检查你的代码库。
  • 控制台可以用 CI services like Jenkins, Travis CI, or GitHub Actions.

写一些文档

  • 使用注释来记录你的代码,尤其是对于复杂或不明显的部分。
  • 请使用描述性和有意义的注释来解释代码片段的目的、行为或用法。
  • 考虑使用Dartdoc等工具生成API文档。

小结

以上的编码准则可以帮助您提高编码标准,增强应用性能,并让您更好地理解最佳实践。通过遵循这些准则,您可以编写更清晰、更易维护的代码,优化应用性能,并避免常见的陷阱。

感谢阅读本文

如果我有什么错?请在评论中让我知道。我很乐意改进。


© 猫哥 ducafecat.com

end

本文由mdnice多平台发布

相关推荐
中年老IT1 天前
从零开始学AI,完成AI 企业知识库的AI问答搭建
程序人生·机器学习
Redamancy_Xun2 天前
开源软件兼容性可信量化分析
java·开发语言·程序人生·网络安全·测试用例·可信计算技术
haojing83122 天前
easegen将教材批量生成可控ppt课件方案设计
程序人生
ly21st3 天前
skywalking配置项indexReplicasNumber不生效问题
程序人生·skywalking
Tiger Z3 天前
R 语言科研绘图第 11 期 --- 柱状图-基础
开发语言·程序人生·r语言·贴图
测试界萧萧3 天前
15:00面试,15:08就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
Redamancy_Xun3 天前
软件老化分析
python·程序人生·安全威胁分析·可信计算技术·安全架构
lijiachang0307183 天前
设计模式(一):单例模式
c++·笔记·学习·程序人生·单例模式·设计模式·大学生
十二测试录4 天前
Jmeter自学【8】- 使用JMeter模拟设备通过MQTT发送数据
经验分享·测试工具·jmeter·程序人生·自动化
拾光师5 天前
MySQL变量
程序人生