Flutter 最佳实践和编码准则

Flutter 最佳实践和编码准则

视频

前语

最佳实践是一套既定的准则,能够进步代码质量、可读性和牢靠性。它们保证遵从行业规范,鼓励一致性,并促进开发人员之间的协作。经过遵从最佳实践,代码变得更简略了解、修改和调试,从而进步整体软件质量。

原文 ducafecat.com/blog/flutte…

参考

dart.dev/effective-d…

正文开端

有许多准则和实践能够采用来进步代码质量和运用功能。

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多平台发布