Flutter 開發規範


想學習好一門編程語言,想標準高效的學習一門編程語言,首先你需要了解它的開發規範。標準的開發規範可以讓我們事半功倍,也可以讓別人更好的理解和使用你的代碼、算法。學習Flutter同樣建議大家先掌握瞭解其開發規範,大致包括:項目結構規範、命名規範、縮進格式規範、註釋規範、代碼規範、其他規範等。良好的開發規範不但有利於提升自己的開發效率,也能夠讓其他人更好的理解你的代碼,並提升自己的編程水平及能力。Flutter的部分編程規範和約束和其他編程語言還是有一些區別,所以本文將着重給大家講解下Flutter開發規範,以便後續的Flutter學習。本文將主要介紹:

  • Flutter的項目結構規範
  • Flutter的命名規範
  • Flutter註釋和格式規範
  • Flutter代碼規範
  • 其他相關規範

Flutter項目結構規範

一個好的項目規範,可以提升項目應用的安全性、穩定性、良好的性能及高用戶體驗等。舉個例子,Google推出了Material Design設計規範,這個設計是經過長時間嘗試與積累形成的,如果大家遵守這個開發與設計規範,就可以讓你的應用在設計上和體驗上更加的方便與人性化。所以高質量的項目開發規範,對我們幫助很大。

那麼接下來,我們就開始Flutter項目結構規範的瞭解與學習。

前面我們講過Flutter的項目結構。
在這裏插入圖片描述
默認新建項目後,官方標準的項目結構如上圖所示。
android目錄存放Android項目結構代碼;ios目錄存放ios項目結構代碼;lib目錄存放Flutter核心的邏輯代碼;test目錄存放測試用例代碼;配置信息寫在pubspec.yaml文件裏。所以,我們一般開發就按照這個目錄結構就可以了,這幾個目錄不能更改。如果我們需要新建個項目內的資源文件目錄,例如我們項目裏需要引入一些打包進去的圖標、圖片文件;音頻視頻文件;字體文件等等,我們參照官方例子,一般把項目資源文件放在項目根目錄自己新建的assets目錄裏,如果有需要,可以在assets目錄裏再進行分類:如images、audios、videos、fonts等等。大致結構如下圖所示:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
如果沒有其他太多的資源,把資源文件都放在assets目錄下即可。這個assets目錄默認是沒有的,我們可以在項目根目錄新建一個assets目錄用於存放應用使用的資源文件。
還有一種就是在項目根目錄裏創建多個類別資源文件夾:如fonts(存放字體文件資源)、assets目錄(存放圖片圖標資源)等。大致如下圖所示:
在這裏插入圖片描述
這兩種資源目錄創建方式都可以,可以根據需求實際情況進行使用。
這裏要注意的是:定義的資源文件,我們需要在pubspec.yaml進行路徑配置,纔可以在Flutter代碼裏使用。
在這裏插入圖片描述
具體的配置信息上節課有詳細講解,這裏就不重複說明了。

接下來我們再看下lib代碼裏的大致結構:
默認創建在lib目錄下只有一個main.dart文件,這是整個應用的入口文件,這個main.dart名稱不可以修改、位置也不可以修改(只能在lib根目錄下)。lib裏的其他類可以按照供能進行劃分目錄,自己建立相應的分類目錄即可。
在這裏插入圖片描述
在這裏插入圖片描述
具體目錄如何劃分,根據自己的實際項目需求和習慣進行劃分即可。
如果想引入第三方庫可以在Dart PUB搜索:https://pub.dartlang.org/ 。然後在pubspec.yaml進行配置即可使用。
在這裏插入圖片描述

Flutter命名規範

大部分編程語言或多或少都有自己的命名特點,不過大同小異。這裏給大家介紹下Flutter的相關命名規範。良好的編碼規範習慣,一致的命名規則有助於我們進行開發。Flutter的命名規範其實也就是Dart語言的規範,後面都以Flutter規範代替。
先看下Flutter的三種命名方式:
1、UpperCamelCase:單詞首字母大寫的駝峯命名方式,例如StudentName;
2、lowerCamelCase:第一個單詞的首字母小寫的駝峯命名方式,如studentName;
3、lowercase_with_underscores:單詞全部是小寫字母,中間用_連接,如student_name。

接下來看下這三種命名方式一般都在哪種情況下使用:
先看下UpperCamelCase命名方式:
UpperCamelCase命名方式一般用在類名、註解、枚舉、typedef和參數的類型上,一般都使用UpperCamelCase命名方式。例如:

//類名命名
class ItemMenu { ... }

class HttpApi { ... }

//註解
@Foo()
class A { ... }

//枚舉
enum Color {
  LightRed, 
  LightBlue
}

//typedef
typedef Predicate<T> = bool Function(T value);

//方法參數類型
@override
Widget build(BuildContext context) {...}

再看下lowerCamelCase命名方式:
lowerCamelCase命名方式一般用在類成員、變量、方法名、參數命名等命名上。如:

//變量命名
var item;

HttpRequest httpRequest;

//方法和參數名稱命名
void align(bool clearItems) {
  // ...
}
//常量名稱定義
const pi = 3.14;
const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');

class Dice {
  static final numberGenerator = Random();
}

最後看下lowercase_with_underscores命名方式:
lowercase_with_underscores命名方式一般用在命名庫(libraries)、包(packages)、目錄(directories)和源文件(source files)上,類似這樣的格式:libray_names, file_names。因此Flutter裏的庫名,包名,目錄和源代碼文件的命名都建議需要採用小寫單詞加_下劃線分隔方式命名。如:

library json_parser.string_scanner;

import 'file_system.dart';
import 'item_menu.dart';

//目錄文件夾命名可以類似:http_utils這種形式
//源代碼文件命名可以類似:screen_utils.dart這種形式

同時,在Flutter導入類庫時候的as關鍵字後面的命名也要遵循lowercase_with_underscores命名方式。如:

import 'dart:math' as math;
import 'package:angular_components/angular_components'
    as angular_components;
import 'package:js/js.dart' as js;

Flutter命名還有一點需要注意的就是不要使用前綴字母,如mHttp,kHttp這種形式。

//推薦
defaultTimeout
//不建議使用
kDefaultTimeout

爲了保持代碼的整潔及有層次分類,我們可以在某些地方使用空行來分隔。

接下來看下導包時候的建議順序:

//建議 dart:包的導入要寫在package:包的前面
import 'dart:async';
import 'dart:html';

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

//建議package:包的導入要寫在我們相對引用本項目類的前面
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'util.dart';

//建議將自己的包內的類的引入放置在其他第三方庫引入的包後面
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'package:my_package/util.dart';

//建議export的引入要寫在import引入的後面
import 'src/error.dart';
import 'src/foo_bar.dart';

export 'src/error.dart';

//同級別的引用排列順序最好按照字母的順序進行排列
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'foo.dart';
import 'foo/foo.dart';

還有一點,如果你的某個方法和常量、變量、類不想被外部其他類調用時用的話,在相應的名稱前加_下劃線前綴即可,例如:

//這樣這個類就不能被其他類訪問調用到了
class _MyMainPageState extends State<MyMainApp> {
  @override
  void initState() {
    super.initState();
  }
  ...

Flutter代碼格式化

很多語言都有自己的格式要求,這樣有利於排版和閱讀使用。其實很多IDE也自帶了一些格式化工具和插件,如Visual Studio Code可以使用Alt+Shift+F進行格式化代碼。那麼接下來就講解下Flutter代碼格式化的相關建議規範:
官方建議可以使用dartfmt進行格式化代碼。dartfmt插件地址:https://github.com/dart-lang/dart_style
這個dartfmt可以幫我們自動按照規範格式化代碼,非常方便。
如果遇到格式化工具都無法格式化的代碼,建議重新簡化組織代碼,如縮短局部變量名稱或更改層級等等。
**官方建議,每行代碼不超過80個字符。**太長的單行顯示不利於閱讀,所以建議不要每行超過80個字符。

建議流程控制相關語句都要加花括號{…},防止出現其他錯誤,也更有利於排版和閱讀。如:

if (isWeekDay) {
  print('Bike to work!');
} else {
  print('Go dancing or read a book!');
}

但是如果一個控制語句只有if,沒有else的話,可以不使用{}:

if (arg == null) return defaultValue;

但是,如果if裏的判斷語句和return的返回的語句內容都很長,可能會產生換行,這種建議要加花括號{…}:

if (overflowChars != other.overflowChars) {
  return overflowChars < other.overflowChars;
}

其他格式化控制需要注意的就是,Flutter採用的是React方式進行開發,所有類都是Widget。如果遇到一些層級嵌套太深的情況下,你也可以將某個層級定義爲另一個方法進行調用引入即可。

class _MyMainPageState extends State<MyMainApp> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('標題'),
        ),
        //通過方法引入
        body: getBody(),
      ),
    );
  }

  Widget getBody() {
    return Center(child: Text("我是內容"));
  }
}

Flutter註釋

Flutter註釋分爲幾種。首先看下//形式單行註釋,這種註釋不會出現、生成在文檔裏,只是代碼裏的註釋:

// 這個註釋不會出現生成到文檔裏
if (_chunks.isEmpty) return false;

greet(name) {
  // 單行註釋,這個註釋不會出現生成到文檔裏
  print('Hi, $name!');
}

接下來是塊註釋(多行註釋),這個可以用來註釋代碼,或者需要多行註釋說明的情況下。

/*
class _MyMainPageState extends State<MyMainApp> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //頁面
      home: Scaffold(
        appBar: AppBar(
          title: Text('標題'),
        ),
        body: getBody(),
      ),
    );
  }

  Widget getBody() {
    return Center(child: Text("我是內容"));
  }
}
*/


greet(name) {
  /* 用多行註釋(塊註釋),來寫單行註釋是不建議的*/
  print('Hi, $name!');
}

最後看下文檔註釋,這個註釋使用///來表示,並且註釋會出現生成到文檔裏。一般我們可以使用文檔註釋來註釋類成員和類型、方法、參數、類、變量常量等:

/// 這個是獲取字符長度
int get length => ...

此時就不建議使用單行註釋了,而是使用文檔註釋,來說明這個成員變量和類型是幹什麼的。

當然還有一種多行註釋也是支持的,只不過Flutter不建議使用。

/**
 * 多行註釋,不建議這種方式,但是也是支持的
 */

編寫註釋時候,建議註釋要精煉簡短;適當的時候可以用空行來分隔註釋內容;不要把註釋內容和周圍的上下文代碼混合在一起,不容易閱讀。
我們也可以在文檔註釋里加入一些dart代碼例子:

 /// A widget to display before the [title].
  ///
  /// If this is null and [automaticallyImplyLeading] is set to true, the
  /// [AppBar] will imply an appropriate widget. For example, if the [AppBar] is
  /// in a [Scaffold] that also has a [Drawer], the [Scaffold] will fill this
  /// widget with an [IconButton] that opens the drawer (using [Icons.menu]). If
  /// there's no [Drawer] and the parent [Navigator] can go back, the [AppBar]
  /// will use a [BackButton] that calls [Navigator.maybePop].
  ///
  /// {@tool sample}
  ///
  /// The following code shows how the drawer button could be manually specified
  /// instead of relying on [automaticallyImplyLeading]:
  ///
  /// ```dart
  /// AppBar(
  ///   leading: Builder(
  ///     builder: (BuildContext context) {
  ///       return IconButton(
  ///         icon: const Icon(Icons.menu),
  ///         onPressed: () { Scaffold.of(context).openDrawer(); },
  ///         tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
  ///       );
  ///     },
  ///   ),
  /// )
  /// ```
  /// {@end-tool}
  ///
  /// The [Builder] is used in this example to ensure that the `context` refers
  /// to that part of the subtree. That way this code snippet can be used even
  /// inside the very code that is creating the [Scaffold] (in which case,
  /// without the [Builder], the `context` wouldn't be able to see the
  /// [Scaffold], since it would refer to an ancestor of that widget).
  ///
  /// See also:
  ///
  ///  * [Scaffold.appBar], in which an [AppBar] is usually placed.
  ///  * [Scaffold.drawer], in which the [Drawer] is usually placed.
  final Widget leading;

可以在文檔註釋中,適當的引入[]方括號來強調某個變量、參數、類或者其他的東西。並且我們的文檔註釋要寫在註解前:

/// A button that can be flipped on and off.
@Component(selector: 'toggle')
class ToggleComponent {}

Flutter也支持在文檔註釋里加入MarkDown文本:

/// 這個是正常的文字
///
///
/// * MarkDown符號
/// * MarkDown符號
/// * MarkDown符號
///
/// 1. MarkDown列表
/// 2. MarkDown列表
/// 1. MarkDown列表
///
///     * MarkDown列表
///     * MarkDown列表
///     * MarkDown列表
///
/// MarkDown語法都支持:
///
/// ```
/// this.code
///     .will
///     .retain(its, formatting);
/// ```
///
/// The code language (for syntax highlighting) defaults to Dart. You can
/// specify it by putting the name of the language after the opening backticks:
///
/// ```html
/// <h1>HTML is magical!</h1>
/// ```
///
/// Links can be:
///
/// * http://www.just-a-bare-url.com
/// * [with the URL inline](http://google.com)
/// * [or separated out][ref link]
///
/// [ref link]: http://google.com
///
/// # A Header
///
/// ## A subheader
///
/// ### A subsubheader
///
/// #### If you need this many levels of headers, you're doing it wrong

但是我們應該避免過度使用MarkDown,這樣可能會導致文檔註釋非常混亂,不利於閱讀使用。
代碼縮減問題可以使用’'來解決:

/// You can use [CodeBlockExample] like this:
///
/// ```
/// var example = CodeBlockExample();
/// print(example.isItGreat); // "Yes."
/// ```

//這種有縮進空格的不建議

/// You can use [CodeBlockExample] like this:
///
///     var example = CodeBlockExample();
///     print(example.isItGreat); // "Yes."

關於Flutter註釋規範就講解麼多。

Flutter代碼使用規範

Flutter代碼使用規範內容比較多,這裏就說幾個典型的例子。
導包相關:
假如我們包結構如下:

my_package
└─ lib
   ├─ src
   │  └─ utils.dart
   └─ api.dart

在api.dart中想引入scr下的utils.dart類,建議這樣引入:

//相對路徑引入即可
import 'src/utils.dart';

//而不是這樣引入
import 'package:my_package/src/utils.dart';
//不需要加入package,因爲如果後續package名字變了,修改起來非常麻煩

字符串相關:
字符串連接不需要用+號連接,直接挨着寫即可:

raiseAlarm(
    'ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
    ...
'Hello, $name! You are ${year - birth} years old.';

//使用+號連接是錯誤的,不支持的
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
    'parts are overrun by martians. Unclear which are which.');
    ...
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

集合相關:
Flutter的集合類型有這幾種:lists, maps, queues, sets。

//建議用這種方式創建空集合
var points = [];
var addresses = {};

//這種方式創建空集合是不建議的
var points = List();
var addresses = Map();

//當然也可以提供類型參數
var points = <Point>[];
var addresses = <String, Address>{};

//下面這種寫法不建議
var points = List<Point>();
var addresses = Map<String, Address>();

//使用isEmpty和isNotEmpty來判斷集合是否爲空
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');

//不要使用.length方法來判斷是否是空
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

//對於集合轉換,我們可以使用它的鏈式高級方法來轉換
var aquaticNames = animals
    .where((animal) => animal.isAquatic)
    .map((animal) => animal.name);

//集合的循環遍歷建議使用for
for (var person in people) {
  ...
}

//這種forEach寫法不推薦
people.forEach((person) {
  ...
});

//List.from一般用於類型轉換,這兩種方式都可以實現,但是推薦第一種寫法
var copy1 = iterable.toList();
var copy2 = List.from(iterable);

//建議這種寫法
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);

//不建議使用List.from這種寫法
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);


//但是如果改變集合類型,這是可以使用List.from方法
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);

//關於集合過濾
//不建議
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int);
//不建議
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int).cast<int>();
//建議寫法
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType<int>();

函數方法相關:

//建議寫法
void main() {
  localFunction() {
    ...
  }
}
//不建議寫法
void main() {
  var localFunction = () {
    ...
  };
}

//建議寫法
names.forEach(print);
//不建議寫法
names.forEach((name) {
  print(name);
});

//用等號將默認值和參數分隔
//建議寫法
void insert(Object item, {int at = 0}) { ... }
//不建議寫法
void insert(Object item, {int at: 0}) { ... }

//可以使用??兩個問號來判斷是否是null
void error([String message]) {
  stderr.write(message ?? '\n');
}

//不要將變量初始化爲null
//建議寫法
int _nextId;

class LazyId {
  int _id;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}
//不建議寫法
int _nextId = null;

class LazyId {
  int _id = null;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}

//不用寫類成員變量的getter和setter方法,默認是隱藏自帶的
//建議寫法
class Box {
  var contents;
}

//不建議,沒必要的,不用寫類成員變量的getter和setter方法
class Box {
  var _contents;
  get contents => _contents;
  set contents(value) {
    _contents = value;
  }
}

//可以使用final來創建只讀常量,也支持=>簡寫
class Box {
  final contents = [];
}

double get area => (right - left) * (bottom - top);
//=>也就是省略了{...}和return

//不建議重複多次使用this關鍵字
//建議寫法
class Box {
  var value;

  void clear() {
    update(null);
  }

  void update(value) {
    this.value = value;
  }
}
//不建議寫法
class Box {
  var value;

  void clear() {
    this.update(null);
  }

  void update(value) {
    this.value = value;
  }
}

//儘量在聲明中初始化常量
//建議
class Folder {
  final String name;
  final List<Document> contents = [];

  Folder(this.name);
  Folder.temp() : name = 'temporary';
}
//不建議
class Folder {
  final String name;
  final List<Document> contents;

  Folder(this.name) : contents = [];
  Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}
//縮減構造方法初始化寫法
//建議
class Point {
  num x, y;
  Point(this.x, this.y);
}
//不建議
class Point {
  num x, y;
  Point(num x, num y) {
    this.x = x;
    this.y = y;
  }
}

//構造方法裏無需重複聲明參數類型
//建議
class Point {
  int x, y;
  Point(this.x, this.y);
}
//不建議
class Point {
  int x, y;
  Point(int this.x, int this.y);
}

//對於空方法體的構造方法直接寫;結尾
//建議
class Point {
  int x, y;
  Point(this.x, this.y);
}
//不建議
class Point {
  int x, y;
  Point(this.x, this.y) {}
}

//new關鍵字可以不寫,dar2已經支持不寫new關鍵字了
//建議
Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(
        child: Text('Increment'),
      ),
      Text('Click!'),
    ],
  );
}
//不建議
Widget build(BuildContext context) {
  return new Row(
    children: [
      new RaisedButton(
        child: new Text('Increment'),
      ),
      new Text('Click!'),
    ],
  );
}

//無需重複定義const關鍵字
//建議
const primaryColors = [
  Color("red", [255, 0, 0]),
  Color("green", [0, 255, 0]),
  Color("blue", [0, 0, 255]),
];
//不建議
const primaryColors = const [
  const Color("red", const [255, 0, 0]),
  const Color("green", const [0, 255, 0]),
  const Color("blue", const [0, 0, 255]),
];

異常處理相關:

//可以使用rethrow重新處理後拋出異常,以提供給其他後續邏輯處理
//建議
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}
//不建議
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}

異步任務編程相關:

//我們可以使用Future和async、await來進行處理異步編程,async和await最後成對出現
//建議寫法
Future<int> countActivePlayers(String teamName) async {
  try {
    var team = await downloadTeam(teamName);
    if (team == null) return 0;

    var players = await team.roster;
    return players.where((player) => player.isActive).length;
  } catch (e) {
    log.error(e);
    return 0;
  }
}
//不建議寫法
Future<int> countActivePlayers(String teamName) {
  return downloadTeam(teamName).then((team) {
    if (team == null) return Future.value(0);

    return team.roster.then((players) {
      return players.where((player) => player.isActive).length;
    });
  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

//如果有些方法功能沒有用到異步任務,不要加async關鍵字
//建議寫法
Future afterTwoThings(Future first, Future second) {
  return Future.wait([first, second]);
}
//不建議寫法
Future afterTwoThings(Future first, Future second) async {
  return Future.wait([first, second]);
}

//關於數據轉換我們可以用Future裏高級用法來簡化操作
//建議寫法
Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
//建議寫法
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}
//不建議寫法
Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();

  File(path).readAsString().then((contents) {
    completer.complete(contents.contains('bear'));
  });

  return completer.future;
}

//可以適當的使用T泛型
//建議寫法
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is Future<T>) {
    var result = await value;
    print(result);
    return result;
  } else {
    print(value);
    return value as T;
  }
}
//不建議寫法
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is T) {
    print(value);
    return value;
  } else {
    var result = await value;
    print(result);
    return result;
  }
}

關於Flutter代碼規範就講解麼多。

發佈了111 篇原創文章 · 獲贊 146 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章