Flutter 最佳实践和编码准则

Flutter 最佳实践和编码准则

视频

前言

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

原文 ducafecat.com/blog/flutte...

参考

dart.dev/effective-d...

正文开始

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

Naming convention 命名规范

  • 类、枚举、类型定义、混入和扩展的名称应使用大驼峰命名法。
dart 复制代码
# Good 
class ClassName {}

extension ExtensionName on String {}

enum EnumName {}

mixin MixinName{}

typedef FunctionName = void Function();
dart 复制代码
# Bad
class Classname {
}

extension Extensionname on String {
}

enum Enumname {
}
mixin Mixinname{}

typedef Functionname = void Function();
  • Libraries、包、目录和源文件的名称应该使用蛇形命名法(小写字母加下划线)。
dart 复制代码
# Good 
my_package
└─ lib
   └─ bottom_nav.dart

# Bad 
mypackage
└─ lib
   └─ bottom-nav.dart
  • 导入的前缀命名应该使用蛇形命名法(小写字母加下划线)。
dart 复制代码
# Good 
import 'package:dio/dio.dart' as dio;

#Bad 
import 'package:dio/dio.dart' as Dio;
  • 变量、常量、参数和命名参数应该使用小驼峰命名法。
dart 复制代码
# 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);
   }
}
dart 复制代码
# 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);
}
  • 应该遵循适当有意义的命名规范。
dart 复制代码
# Good
Color backgroundColor;
int calculateAge(Date dob);

# Bad
Color bg;
int age(Date date);
  • 私有变量名前面加下划线。
dart 复制代码
class ClassName {

// private variable
String _variableName;
}

使用可空运算符

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

dart 复制代码
# Bad
String? name;
name= name==null ? "unknown": name;

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

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

dart 复制代码
# Bad
String? name;
name= name==null? null: name.length.toString();

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

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

dart 复制代码
# Bad
(person as Person).name="Ashish";

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

避免不必要地创建lambda函数

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

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

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

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

  • 当你已经在另一个集合中存储了现有的项目时,利用扩展集合可以简化代码。
dart 复制代码
# 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(级联)操作符非常适合在同一对象上执行一系列操作,使代码更加简洁易读。
dart 复制代码
class Person {
  String? name;
  int? age;
  Person({
    this.name,
    this.age,
  });

  @override
  String toString() {
    return "name: $name age $age";
  }
}
dart 复制代码
# 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的条件表达式。
dart 复制代码
# Bad
Column(
        children: [
          isLoggedIn
              ? ElevatedButton(
                  onPressed: () {},
                  child: const Text("Go to Login page"),
                )
              : const SizedBox(),
        ],
      ),
dart 复制代码
# Good
Column(
        children: [
         if(isLoggedIn)
           ElevatedButton(
             onPressed: () {},
              child: const Text("Go to Login page"),
            )
        ],
      ),

使用箭头函数

  • 如果一个函数只有一条语句,使用 () => 箭头函数。
dart 复制代码
# 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 库,您可以按以下方式输出日志消息:
dart 复制代码
# Bad 
# production mode

// commented message---main method   
void main(){
	print("print statement"); 
	//..rest of code
}
void unusedFunction(){
}
dart 复制代码
# Good 
# production mode
  
void main(){
//..rest of code
}

正确的文件夹结构

  • 将代码分离到适当的文件夹结构中,包括提供者(providers)、模型(models)、屏幕/页面(screens/pages)、服务(services)、常量(constants)和工具(utils)。
sh 复制代码
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 配置。
yaml 复制代码
include: package:flutter_lints/flutter.yaml
analyzer:
  errors:
    require_trailing_commas: error
linter:
  rules:
    require_trailing_commas: true
    prefer_relative_imports: true
  • 尝试通过在 utils 文件夹中保存的辅助函数中实现代码的可重用性。
dart 复制代码
# 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文件夹中。
dart 复制代码
# 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界面中避免使用静态或硬编码的字符串,建议根据其范围将其组织在单独的文件夹或文件中。
dart 复制代码
# 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 
          )
        ],
      ),
    );
  }
}
dart 复制代码
#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的子树的部分。
dart 复制代码
# 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"))
        ],
      ),
    );
  }
}
dart 复制代码
# 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 打印语句
dart 复制代码
# 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。
  • 业务逻辑应该与用户界面分离。
dart 复制代码
# 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),
      ),
    );
  }
}
dart 复制代码
# 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 这样的日志记录库来记录重要事件或错误。
dart 复制代码
# 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
  }
}
dart 复制代码
# 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 这样的测试框架来编写和运行测试。
  • 追求高代码覆盖率,尤其是对于应用程序的关键部分。
dart 复制代码
# 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的最佳实践,例如创建有意义的提交信息和分支策略。
dart 复制代码
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多平台发布

相关推荐
我是陈泽2 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
肖哥弹架构1 天前
Spring 全家桶使用教程
java·后端·程序员
IT杨秀才4 天前
自己动手写了一个协程池
后端·程序员·go
程序员麻辣烫6 天前
像AI一样思考
程序员
一颗苹果OMG7 天前
关于进游戏公司实习的第一周
前端·程序员
万少8 天前
你会了吗 HarmonyOS Next 项目级别的注释规范
前端·程序员·harmonyos
楽码8 天前
彻底理解时间?在编程中使用原子钟
后端·算法·程序员
江南一点雨9 天前
又一家培训机构即将倒闭!打工人讨薪无果,想报名的小伙伴擦亮眼睛~
java·程序员
用户86178277365189 天前
ELK 搭建 & 日志集成
java·后端·程序员
河北小田9 天前
局部变量成员变量、引用类型、this、static
java·后端·程序员