在前兩篇文章中,我首先與你一起學習了 Dart 程序的基本結構和語法,認識了 Dart 語言世界的基本構成要素,也就是類型系統,以及它們是怎么表示信息的。然后,我帶你學習了 Dart 面向對象設計的基本思路,知道了函數、類與運算符這些其他編程語言中常見的概念,在 Dart 中的差異及典型用法,理解了 Dart 是怎么處理信息的。
可以看到,Dart 吸納了其他編程語言的優點,在關于如何表達以及處理信息上,既簡單又簡潔,而且又不失強大。俗話說,紙上得來終覺淺,絕知此事要躬行。那么今天,我就用一個綜合案例,把前面學習的關于 Dart 的零散知識串起來,希望你可以動手試驗一下這個案例,借此掌握如何用 Dart 編程。
有了前面學習的知識點,再加上今天的綜合案例練習,我認為你已經掌握了 Dart 最常用的 80% 的特性,可以在基本沒有語言障礙的情況下去使用 Flutter 了。至于剩下的那 20% 的特性,因為使用較少,所以我不會在本專欄做重點講解。如果你對這部分內容感興趣的話,可以訪問[官方文檔](https://dart.dev/tutorials)去做進一步了解。
此外,關于 Dart 中的異步和并發,我會在后面的第 23 篇文章“單線程模型怎么保證 UI 運行流暢?”中進行深入介紹。
## 案例介紹
今天,我選擇的案例是,先用 Dart 寫一段購物車程序,但先不使用 Dart 獨有的特性。然后,我們再以這段程序為起點,逐步加入 Dart 語言特性,將其改造為一個符合 Dart 設計思想的程序。你可以在這個改造過程中,進一步體會到 Dart 的魅力所在。
首先,我們來看看在不使用任何 Dart 語法特性的情況下,一個有著基本功能的購物車程序長什么樣子。
~~~
// 定義商品 Item 類
class Item {
double price;
String name;
Item(name, price) {
this.name = name;
this.price = price;
}
}
// 定義購物車類
class ShoppingCart {
String name;
DateTime date;
String code;
List<Item> bookings;
price() {
double sum = 0.0;
for(var i in bookings) {
sum += i.price;
}
return sum;
}
ShoppingCart(name, code) {
this.name = name;
this.code = code;
this.date = DateTime.now();
}
getInfo() {
return '購物車信息:' +
'\n-----------------------------' +
'\n 用戶名: ' + name+
'\n 優惠碼: ' + code +
'\n 總價: ' + price().toString() +
'\n 日期: ' + date.toString() +
'\n-----------------------------';
}
}
void main() {
ShoppingCart sc = ShoppingCart('張三', '123456');
sc.bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)];
print(sc.getInfo());
}
~~~
在這段程序中,我定義了商品 Item 類,以及購物車 ShoppingCart 類。它們分別包含了一個初始化構造方法,將 main 函數傳入的參數信息賦值給對象內部屬性。而購物車的基本信息,則通過 ShoppingCart 類中的 getInfo 方法輸出。在這個方法中,我采用字符串拼接的方式,將各類信息進行格式化組合后,返回給調用者。
運行這段程序,不出意外,購物車對象 sc 包括的用戶名、優惠碼、總價與日期在內的基本信息都會被打印到命令行中。
~~~
購物車信息:
-----------------------------
用戶名: 張三
優惠碼: 123456
總價: 30.0
日期: 2019-06-01 17:17:57.004645
-----------------------------
~~~
這段程序的功能非常簡單:我們初始化了一個購物車對象,然后給購物車對象進行加購操作,最后打印出基本信息。可以看到,在不使用 Dart 語法任何特性的情況下,這段代碼與 Java、C++ 甚至 JavaScript 沒有明顯的語法差異。
在關于如何表達以及處理信息上,Dart 保持了既簡單又簡潔的風格。那接下來,**我們就先從表達信息入手,看看 Dart 是如何優化這段代碼的。**
## 類抽象改造
我們先來看看 Item 類與 ShoppingCart 類的初始化部分。它們在構造函數中的初始化工作,僅僅是將 main 函數傳入的參數進行屬性賦值。
在其他編程語言中,在構造函數的函數體內,將初始化參數賦值給實例變量的方式非常常見。而在 Dart 里,我們可以利用語法糖以及初始化列表,來簡化這樣的賦值過程,從而直接省去構造函數的函數體:
~~~
class Item {
double price;
String name;
Item(this.name, this.price);
}
class ShoppingCart {
String name;
DateTime date;
String code;
List<Item> bookings;
price() {...}
// 刪掉了構造函數函數體
ShoppingCart(this.name, this.code) : date = DateTime.now();
...
}
~~~
這一下就省去了 7 行代碼!通過這次改造,我們有兩個新的發現:
* 首先,Item 類與 ShoppingCart 類中都有一個 name 屬性,在 Item 中表示商品名稱,在 ShoppingCart 中則表示用戶名;
* 然后,Item 類中有一個 price 屬性,ShoppingCart 中有一個 price 方法,它們都表示當前的價格。
考慮到 name 屬性與 price 屬性(方法)的名稱與類型完全一致,在信息表達上的作用也幾乎一致,因此我可以在這兩個類的基礎上,再抽象出一個新的基類 Meta,用于存放 price 屬性與 name 屬性。
同時,考慮到在 ShoppingCart 類中,price 屬性僅用做計算購物車中商品的價格(而不是像 Item 類那樣用于數據存取),因此在繼承了 Meta 類后,我改寫了 ShoppingCart 類中 price 屬性的 get 方法:
~~~
class Meta {
double price;
String name;
Meta(this.name, this.price);
}
class Item extends Meta{
Item(name, price) : super(name, price);
}
class ShoppingCart extends Meta{
DateTime date;
String code;
List<Item> bookings;
double get price {...}
ShoppingCart(name, this.code) : date = DateTime.now(),super(name,0);
getInfo() {...}
}
~~~
通過這次類抽象改造,程序中各個類的依賴關系變得更加清晰了。不過,目前這段程序中還有兩個冗長的方法顯得格格不入,即 ShoppingCart 類中計算價格的 price 屬性 get 方法,以及提供購物車基本信息的 getInfo 方法。接下來,我們分別來改造這兩個方法。
## 方法改造
我們先看看 price 屬性的 get 方法:
~~~
double get price {
double sum = 0.0;
for(var i in bookings) {
sum += i.price;
}
return sum;
}
~~~
在這個方法里,我采用了其他語言常見的求和算法,依次遍歷 bookings 列表中的 Item 對象,累積相加求和。
而在 Dart 中,這樣的求和運算我們只需重載 Item 類的“+”運算符,并通過對列表對象進行歸納合并操作即可實現(你可以想象成,把購物車中的所有商品都合并成了一個商品套餐對象)。
另外,由于函數體只有一行,所以我們可以使用 Dart 的箭頭函數來進一步簡化實現函數:
~~~
class Item extends Meta{
...
// 重載了 + 運算符,合并商品為套餐商品
Item operator+(Item item) => Item(name + item.name, price + item.price);
}
class ShoppingCart extends Meta{
...
// 把迭代求和改寫為歸納合并
double get price => bookings.reduce((value, element) => value + element).price;
...
getInfo() {...}
}
~~~
可以看到,這段代碼又簡潔了很多!接下來,我們再看看 getInfo 方法如何優化。
在 getInfo 方法中,我們將 ShoppingCart 類的基本信息通過字符串拼接的方式,進行格式化組合,這在其他編程語言中非常常見。而在 Dart 中,我們可以通過對字符串插入變量或表達式,并使用多行字符串聲明的方式,來完全拋棄不優雅的字符串拼接,實現字符串格式化組合。
~~~
getInfo () => '''
購物車信息:
-----------------------------
用戶名: $name
優惠碼: $code
總價: $price
Date: $date
-----------------------------
''';
~~~
在去掉了多余的字符串轉義和拼接代碼后,getInfo 方法看著就清晰多了。
在優化完了 ShoppingCart 類與 Item 類的內部實現后,我們再來看看 main 函數,從調用方的角度去分析程序還能在哪些方面做優化。
## 對象初始化方式的優化
在 main 函數中,我們使用
~~~
ShoppingCart sc = ShoppingCart('張三', '123456') ;
~~~
初始化了一個使用‘123456’優惠碼、名為‘張三’的用戶所使用的購物車對象。而這段初始化方法的調用,我們可以從兩個方面優化:
* 首先,在對 ShoppingCart 的構造函數進行了大量簡寫后,我們希望能夠提供給調用者更明確的初始化方法調用方式,讓調用者以“參數名: 參數鍵值對”的方式指定調用參數,讓調用者明確傳遞的初始化參數的意義。在 Dart 中,這樣的需求,我們在聲明函數時,可以通過給參數增加{}實現。
* 其次,對一個購物車對象來說,一定會有一個有用戶名,但不一定有優惠碼的用戶。因此,對于購物車對象的初始化,我們還需要提供一個不含優惠碼的初始化方法,并且需要確定多個初始化方法與父類的初始化方法之間的正確調用順序。
按照這樣的思路,我們開始對 ShoppingCart 進行改造。
需要注意的是,由于優惠碼可以為空,我們還需要對 getInfo 方法進行兼容處理。在這里,我用到了 a??b 運算符,這個運算符能夠大量簡化在其他語言中三元表達式 (a != null)? a : b 的寫法:
~~~
class ShoppingCart extends Meta{
...
// 默認初始化方法,轉發到 withCode 里
ShoppingCart({name}) : this.withCode(name:name, code:null);
//withCode 初始化方法,使用語法糖和初始化列表進行賦值,并調用父類初始化方法
ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name,0);
//?? 運算符表示為 code 不為 null,則用原值,否則使用默認值 " 沒有 "
getInfo () => '''
購物車信息:
-----------------------------
用戶名: $name
優惠碼: ${code??" 沒有 "}
總價: $price
Date: $date
-----------------------------
''';
}
void main() {
ShoppingCart sc = ShoppingCart.withCode(name:'張三', code:'123456');
sc.bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)];
print(sc.getInfo());
ShoppingCart sc2 = ShoppingCart(name:'李四');
sc2.bookings = [Item('香蕉',15.0), Item('西瓜',40.0)];
print(sc2.getInfo());
}
~~~
運行這段程序,張三和李四的購物車信息都會被打印到命令行中:
~~~
購物車信息:
-----------------------------
用戶名: 張三
優惠碼: 123456
總價: 30.0
Date: 2019-06-01 19:59:30.443817
-----------------------------
購物車信息:
-----------------------------
用戶名: 李四
優惠碼: 沒有
總價: 55.0
Date: 2019-06-01 19:59:30.451747
-----------------------------
~~~
關于購物車信息的打印,我們是通過在 main 函數中獲取到購物車對象的信息后,使用全局的 print 函數打印的,我們希望把打印信息的行為封裝到 ShoppingCart 類中。而對于打印信息的行為而言,這是一個非常通用的功能,不止 ShoppingCart 類需要,Item 對象也可能需要。
因此,我們需要把打印信息的能力單獨封裝成一個單獨的類 PrintHelper。但,ShoppingCart 類本身已經繼承自 Meta 類,考慮到 Dart 并不支持多繼承,我們怎樣才能實現 PrintHelper 類的復用呢?
這就用到了我在上一篇文章中提到的“混入”(Mixin),相信你還記得只要在使用時加上 with 關鍵字即可。
我們來試著增加 PrintHelper 類,并調整 ShoppingCart 的聲明:
~~~
abstract class PrintHelper {
printInfo() => print(getInfo());
getInfo();
}
class ShoppingCart extends Meta with PrintHelper{
...
}
~~~
經過 Mixin 的改造,我們終于把所有購物車的行為都封裝到 ShoppingCart 內部了。而對于調用方而言,還可以使用級聯運算符“..”,在同一個對象上連續調用多個函數以及訪問成員變量。使用級聯操作符可以避免創建臨時變量,讓代碼看起來更流暢:
~~~
void main() {
ShoppingCart.withCode(name:'張三', code:'123456')
..bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)]
..printInfo();
ShoppingCart(name:'李四')
..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
..printInfo();
}
~~~
很好!通過 Dart 獨有的語法特性,我們終于把這段購物車代碼改造成了簡潔、直接而又強大的 Dart 風格程序。
## 總結
這就是今天分享的全部內容了。在今天,我們以一個與 Java、C++ 甚至 JavaScript 沒有明顯語法差異的購物車雛形為起步,逐步將它改造成了一個符合 Dart 設計思想的程序。
首先,我們使用構造函數語法糖及初始化列表,簡化了成員變量的賦值過程。然后,我們重載了“+”運算符,并采用歸納合并的方式實現了價格計算,并且使用多行字符串和內嵌表達式的方式,省去了無謂的字符串拼接。最后,我們重新梳理了類之間的繼承關系,通過 mixin、多構造函數,可選命名參數等手段,優化了對象初始化調用方式。
下面是今天購物車綜合案例的完整代碼,希望你在 IDE 中多多練習,體會這次的改造過程,從而對 Dart 那些使代碼變得更簡潔、直接而強大的關鍵語法特性產生更深刻的印象。同時,改造前后的代碼,你也可以在 GitHub 的[Dart\_Sample](https://github.com/cyndibaby905/08_Dart_Sample)中找到:
~~~
class Meta {
double price;
String name;
// 成員變量初始化語法糖
Meta(this.name, this.price);
}
class Item extends Meta{
Item(name, price) : super(name, price);
// 重載 + 運算符,將商品對象合并為套餐商品
Item operator+(Item item) => Item(name + item.name, price + item.price);
}
abstract class PrintHelper {
printInfo() => print(getInfo());
getInfo();
}
//with 表示以非繼承的方式復用了另一個類的成員變量及函數
class ShoppingCart extends Meta with PrintHelper{
DateTime date;
String code;
List<Item> bookings;
// 以歸納合并方式求和
double get price => bookings.reduce((value, element) => value + element).price;
// 默認初始化函數,轉發至 withCode 函數
ShoppingCart({name}) : this.withCode(name:name, code:null);
//withCode 初始化方法,使用語法糖和初始化列表進行賦值,并調用父類初始化方法
ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name,0);
//?? 運算符表示為 code 不為 null,則用原值,否則使用默認值 " 沒有 "
@override
getInfo() => '''
購物車信息:
-----------------------------
用戶名: $name
優惠碼: ${code??" 沒有 "}
總價: $price
Date: $date
-----------------------------
''';
}
void main() {
ShoppingCart.withCode(name:'張三', code:'123456')
..bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)]
..printInfo();
ShoppingCart(name:'李四')
..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
..printInfo();
}
~~~
## 思考題
請你擴展購物車程序的實現,使得我們的購物車可以支持:
1. 商品數量屬性;
2. 購物車信息增加商品列表信息(包括商品名稱,數量及單價)輸出,實現小票的基本功能。
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略