Flutter 开发规范

553 阅读12分钟

TL;DR

  • 禁止 出现任何错误;
  • 禁止 不要写 new ,在 Dart 2.x 中已不建议继续使用 new 关键字;
  • 必须 缩进为2空格;
  • 必须 使用 flutter format 、 dartfmt 或者 dart format 对代码进行格式化;
  • 必须 使用 Flutter 官方仓库下的 analysis_options.yaml (文件)进行基本代码规范约束。
  • 必须 在较长代码和嵌套中使用尾逗号进行拆分,防止超过80字或出现 )))) ;
  • 尽量 避免出现任何警告;
  • 尽量 避免一行超过80个字符。如果超过了,请检查是否有 )))) ;
  • 尽量 避免直接修改 Flutter 源码,或依赖的 package 源码;
  • 尽量 在项目排期允许的情况下,将所有 packages 保持为可使用的最新版本;
  • 推荐 在项目排期允许的情况下,将 Flutter SDK 保持为最新的稳定版本

除此之外,以下是各类规范的细则。

1. 文件类规范

1.1 命名规范

对于一个项目而言,良好的文件和目录层级可读性,保证了代码的健壮。

一个普通的 Flutter 项目的目录层级例子如下:

--flutter_project           # 项目根目录

----/android, ios           # 移动端原生文件
----/web, macos, windows    # Web、桌面平台层文件

----/assets                 # 资源文件
------/fonts                # 字体
------/images               # 图片
------/others               # 其他类型资源

----/lib                    # 项目主要代码
------/apis                 # 接口定义,各类请求封装
------/constants            # 常量、统一引入及导出口径
--------/constants.dart     # 各种 package、models、apis、widgets 最终统一到此处导出
--------/resources.dart     # (可选) 将资源引用转换成常量 (flutter_asset_generator)
------/extension            # 项目内定义的扩展方法
------/models               # Beans、Models,类定义
--------/models.dart        # 所有内容作为 partition 进行分块
------/pages                # 页面
------/providers            # (可选) 状态保持
------/utils                # 工具类
------/widgets              # 自定义 widget

----/analysis_options.yaml  # IDE 代码分析配置

----/main.dart              # 主入口

----/pubspec.lock           # 当前依赖到的 packages 版本
----/pubspec.yaml           # Pub packages 声明文件

1.1.1 文件命名

  • 必须 所有文件(包括资源)采用小写+下划线命名。

    • home_page.dart 
  • 必须 功能组命名时,如果代表一系列同类的集合,使用复数词语。

    • enums , models 
  • 尽量 不要使用与项目无关没有共识缩写用于文件命名。

    • wlck_page.dart
  • 尽量 分词时信、雅、达。

    • user_edit_profile_page.dart

1.1.2 类命名

一个类应直截了当地表明其内容+身份+用途

  • 必须 使用大驼峰命名。同样适用于枚举扩展函数

    • class Foo , extension Bar<T> 
  • 必须 超过两个字母的大写缩略词当做一般单词对待。两个字母的单词除外。

    • HttpConnectionInfo , IOStream , Id  ✅
    • HTTPConnection , IoStream , ID  🚫
  • 必须 在导入库时,使用小写+下划线的别名。

    • import 'dart:math' as math 
  • 尽量 不要超过5个单词。如果超过了,应该考虑是否为命名或拆分不当。

1.1.3 变量命名

  • 必须 使用小驼峰命名。

    • int imagesLength 
    • List<AssetEntity> selectedAssets 
  • 必须 不要超过5个单词。

  • 必须 布尔类型变量使用前缀 is 或 should 。

    • bool isLoading
  • 必须 如果一个变量不是私有变量,不要使用 _ 作为前缀,因为 Dart 中没有私有的概念。

  • 尽量 除了表示状态的布尔值,其他变量使用名词进行命名。

    • double width 
    • Widget separator 
  • 尽量 不要使用前缀字母。( k 是 Flutter 内定的前缀。)

    • kDefaultTimeout 
  • 尽量 不要同时命名多个接近且具有迷惑性的变量。

    • tabIndex , tabCurrentIndex 

2 用法规范

2.1 import/export

  • 必须 按照字母表顺序进行 import/export。
  • 必须 将 dart: 放在最前。
  • 必须 将 package: 放在相对引用之前。
  • 必须 将 Flutter 的 package 放在其他 package 之前。
  • 必须 将 export 放在所有 import 之后。
  • 尽量 避免在构建一般页面时,将 package 的 src 引入。除非你正在对一个官方组件进行定制。implementation_imports

2.2 构造方法

构造方法是 OOP 语言中最常用的方法之一。在 Dart + Flutter 中,依照各项规则的限制,一般对构造函数有以下几点要求:

  • 必须 所有构造函数工厂方法都要写在类的开头位置,而后才是变量声明。
  • 必须 StatefulWidget 的构造字段必须为终值 final 。(@immutable)
  • 必须 Widget 的构造必须带有命名可选参数 Key key 。
  • 必须 如果构造需要3个及以上的参数,或作为一个 Widget 的构造,请将它们转为命名可选参数。
  • 必须 单例使用私有构造。(例: const API._() )
  • 必须 为所有自定义的类实现 toString() 。
  • 尽量 如果 Widget 的字段均为终值且没有构造回调,将构造方法加上 const 。
  • 尽量 有可能用于 Iterable*  的类,请重写 operator == 和 hashCode 。
  • 推荐 作为 Provider Model 的 ChangeNotifier ,字段使用 getter & setter。
  • 推荐 一个由 json 数据转换而来的实体类,请实现 Map<String, dynamic> toJson() 。

根据上述要求举两个例子:

class Person {
  const Person({
    this.name = 'Alex',
    this.age = 23,
    this.skills,
  });

  factory Person.fromJson(Map<String, dynamic?> json) {
    return Person(
      name: json['name'] as String,
      age: json['age'] as int,
      skills: json['skills'] as List<String>?,
    );
  }

  final String name;
  final int age;
  final List<String>? skills;

  @override
  bool operator ==(Object other) {
    if (identical(other, this)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return name == other.name
        && age == other.age
        && skills == other.skills;
  }

  @override
  int get hashCode => hashValues(name, age, skills);
}
class PersonalPage extends StatefulWidget {
  const PersonalPage({
    Key key,
    @required this.person,
  }) : super(key: key);

  final Person person;

  ///...///
}

2.3 变量声明、使用、调用

  • 禁止 不要使用 var , Object ,在确定变量时必须确定它的类型

  • 禁止 不要使用魔数。如果它是一种类型表示,直接将它们定义为枚举

  • 必须 避免普遍地使用缩写,除非是类似表示偏移的 x 。

    • i -> index 
  • 必须 在声明时将类型写明,包括泛型always_specify_types

    • Map<String, dynamic> json 
    • Column(children: <Widget>[]) 
  • 必须 使用 final 声明无需改动基本类型无需重新创建实例非基础类型的变量。

    • final int userId = 0 
    • final ScrollController controller = ScrollController() 
    • final List<AssetEntity> selectedAssets = <AssetEntity>[] 
  • 必须 空安全 除了 Model 以外,不要滥用 ? ,在声明时就确定是否可空。

2.3.1 布尔值

  • 禁止 不要写布尔转布尔的语句。

    • return list.isEmpty ? true : false
  • 必须 非空安全 使用 ?? 将可能为空的布尔值转换为不为空的值。

    • list?.isEmpty ?? true 
  • 尽量 为值起肯定形式的名字。如果在使用它时总是取反,请考虑取反向意义的名字。

2.3.2 枚举

  • 必须 switch 中列出所有的枚举值,不要用 default 。
  • 必须 使用 switch 而不是 if 来判断枚举内容。

2.3.3 字符串

2.3.4 集合

  • 必须 List , Set , Map 使用字面量构造实例。prefer_collection_literals

    • <T>[] , <T>{} , <T, S>{} 
  • 尽量 Iterable* 初始化为空的集合。

    • List<int> list = <int>[] 
  • 尽量 避免使用 toList() 和 List.cast ,使用 List.from 。

    • 仅在改变类型和从可迭代对象生成 List 时使用,其他时间勿用。
  • 尽量 使用展开操作符构造新的 Iterable 。

    • final List<int> anotherList = <int>[...list] 
  • 推荐 使用级联操作符对同一个对象进行连续调用。

    • list..add(x)..add(y)

2.3.5 局部变量

2.3.6 异步类

  • 必须 StreamController 如果不再使用必须 close
  • 必须 监听 Stream 产生的 StreamSubscription 不再收听必须手动调用 cancel 取消监听。

模块化

在实际的项目中,我们需要重点关注业务点,尽量将一个单独的业务点构建成一个模块,不同模块之间使用框架进行通信,并注重不同模块之间的解耦。

一般的应用可以分成共享模块与业务模块两大类别。业务模块依赖共享模块来使用公用的功能,例如存储等。而具体的业务模块之间也可能存在依赖的关系,我们需要在每个业务模块中定义需要供其他模块使用的接口,并供其他的业务模块使用。

同时,业务模块应当遵循可插拔的原则,即可以随时的进行替换,这需要用到动态调用的技术,即不同模块之间使用协议进行通信,而不需要进行相互依赖。

这一部分仍在探索中,等待后续在实际项目的开发中继续完善和迭代。

插件化

插件化与组件化不同,其提供的是整个应用的扩展能力,插件分为宿主、协议和插件内容三个部分,在实际的项目中,一般使用插件来提供额外的功能,比如新的界面和业务、数据的后处理等。

我们需要根据实际的项目来定义插件的扩展,输入和输出,界面等。

目前插件化在Flutter项目中的应用较少,我们可以参考Android项目的插件化开发。

项目结构

大致的项目结构如下,实际的项目结构会根据项目做出具体的调整或者修改。

lua
复制代码
Project
└─ android
└─ lib
   └─ bean
   └─ config
   └─ dao
   └─ entity
   └─ model
   └─ io
      └─ network
      └─ database
      └─ file
   └─ ui
      └─ page
      └─ component
      └─ widget
   └─ util
   └─ viewmodel
   └─ main.dart
└─ linux
└─ macos
└─ moduleA
└─ moduleB
└─ web
└─ windows
└─ .gitignore
└─ .metadata
└─ analysis_options.yaml
└─ pubspec.lock
└─ pubspec.yaml
└─ README.md

项目的分层

项目的分层是实现关注点分离的常用手段,通常我们使用分层来实现不同业务的解耦:例如视图和逻辑的解耦。

在实际项目中,我们可以将一个项目分层五层:视图层(UI)、视图模型层(ViewModel)、业务层(Model)、仓储层(Repo)、持久层(Database、File、Network etc)

视图层

视图层在Flutter项目中指的是UI界面,视图层应当仅关注界面相关的逻辑,并使用视图模型层的数据呈现界面。

注意:视图层只能使用内部数据或者视图模型层提供的数据。

视图层不应当包含除了界面显示依赖的任何业务逻辑,除了使用判断表达式来区分需要显示的内容等。也不应该在视图层中直接修改界面,而应当向视图模型层/模型层提交Intent事件来修改页面数据。

视图层使用单向数据流进行渲染,也就是说,界面不能直接修改任何外部数据,而是要通过Intent事件接口的方式交给其他层处理,并进行视图的更新。

例外:当视图中包含动画、内部数据时,可以直接在空间的内部进行变更。

视图模型层

视图模型层用于向界面提供直接的数据,其数据来源主要有:

  1. 视图模型层内部定义的与视图直接关联的数据。(仅供界面独有)
  2. 模型层定义的与业务相关的运行时数据。(业务相关)
  3. 仓储层定义的持久化数据。

需要注意的是,无论数据的来源在哪,同一概念的数据应当具有单一的来源,从而避免数据来源的不一致性。

模型层

模型层设计单独的业务逻辑,提供数据处理,具体的业务逻辑相关的功能。模型层可以提供业务相关的运行时数据,模型层主要的功能有:

  1. 向视图模型层、视图提供数据来源。
  2. 存储与业务相关的运行时数据。
  3. 接收来自视图模型层的事件,并进行数据处理。
  4. 将数据的修改提交至仓储层。

仓储层

仓储层是持久化的入口点,所有运行时数据与外部数据交互的逻辑均在仓储层中实现,仓储层可以从多个来源获取数据,也可以向多个来源提交数据。仓储层可以对接的数据来源包括:

  1. 本地数据库,在实际项目中,一般指sqlite数据库。
  2. 网络接口,从网络中获取必要的数据等。
  3. 文件,一般仓储层可以将数据放入到文件中。
  4. 嵌入式设备接口,也需要从仓储层写入或者读取数据。

仓储层的设计不关心其距离的数据来源,向上层提供统一的持久层数据接口,从而提升代码的可维护性

相关概念

推荐:使用流数据来获取界面的状态

流数据,即Stream<T>,可以自动的获取最新的数据和状态,而不需要进行手动的修改,因此,推荐使用流数据来渲染界面,以及处理数据。

流数据是自动的,不需要进行手动提交,因此,其能极大程度上避免数据不一致的情况发生。

流数据的概念和RectiveX框架比较类似,可以对比着进行学习和使用。

框架

在本部分介绍考虑的框架并说明选择该框架的理由

状态管理

计划采用BLoC作为状态管理的框架。

对于比较大型的项目来说,使用统一的状态管理框架是十分有必要的,目前Flutter状态管理的框架可以参考的有:

  • Provider
  • Riverpod
  • setState
  • InheritedWidget & Inherited Model
  • Redux
  • Fish-Redux
  • BLoC/Rx

目前,打算使用BLoC作为状态管理的主要框架,主要考虑到其具有完整的设计逻辑、而且可以很好的结合流数据的处理逻辑,比较适合用该框架做大型项目的开发。 而且BLoC也是目前更新频率较高,而且一致在维护和更新的框架。

具体的使用方式可以参考flutter_bloc使用解析 - 掘金

pub地址:flutter_bloc | Flutter Package (pub.dev) 当前版本 8.1.2 5339Likes

github地址:felangel/bloc

序列化

Json序列化目前主流的有json_serializablebuilt_value两个插件。前者更加成熟,后者的功能更加完善以及强大。

目前打算使用built value作为序列化主要使用的代码生成工具。后续继续研究其如何序列化或者反序列化成protobuf

pub地址:json_serializable | Dart Package (pub.dev)

pub地址:built_value | Dart Package (pub.dev)

ORM框架

传统的sqflite需要手写sql代码,是不符合实际开发的效率需求的,因此,我们需要使用ORM框架来简化程序与数据库进行交互,目前主要考虑的是floor框架。

具体使用方式可以参考:Flutter floor数据库类orm插件使用详解 - 掘金

pub地址:floor | Flutter Package (pub.dev)