想學習好一門編程語言,想標準高效的學習一門編程語言,首先你需要了解它的開發規範。標準的開發規範可以讓我們事半功倍,也可以讓別人更好的理解和使用你的代碼、算法。學習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代碼規範就講解麼多。