本文介紹了Flutter應用程序中Widget,State,BuildContext和InheritedWidget的重要概念。
特別注意InheritedWidget,它是最重要且記錄較少的小部件之一。
本文內容很長,但做技術就是要沉得下心!
難度:初學者
前言
Flutter中Widget,State和BuildContext的概念是每個Flutter開發人員需要完全理解的最重要概念之一。
但是,文檔很龐大,并不總是清楚地解釋這個概念。
我會用自己的話語和捷徑來解釋這些概念,本文的真正目的是試圖澄清以下主題:
- 有狀態和無狀態小部件之間的區別
- 什么是BuildContext
- 什么是State以及如何使用它
- BuildContext與其State對象之間的關系
- InheritedWidget以及在Widgets樹中傳播信息的方式
- 重建的概念
第1部分:概念
小工具的概念
在Flutter中,幾乎所有東西都是Widget。
將Widget視為可視組件(或與應用程序的可視方面交互的組件)。
當您需要構建與布局直接或間接相關的任何內容時,您正在使用窗口小部件。
小部件樹的概念
窗口小部件以樹形結構組織。
包含其他小部件的小部件稱為父Widget(或Widget容器)。
包含在父窗口小部件中的窗口小部件稱為子窗口小部件。
讓我們用Flutter自動生成的基本應用程序來說明這一點。
這是簡化的代碼,僅限于構建方法:
@override
Widget build(BuildContext){
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
如果我們現在觀察這個基本示例,我們將看到以下Widgets樹結構(限制代碼中存在的Widgets列表):
[站外圖片上傳中...(image-c411a3-1539427274357)]
BuildContext的概念
另一個重要的概念是BuildContext。
BuildContext只不過是對構建的所有窗口小部件的樹結構中的窗口小部件的位置的引用。
簡而言之,將BuildContext視為Widgets樹的一部分,Widget將附加到此樹。
一個BuildContext只屬于一個小部件。
如果窗口小部件“A”具有子窗口小部件,則窗口小部件“A”的BuildContext將成為直接子窗口BuildContexts的父BuildContext。
閱讀本文,很明顯BuildContexts是鏈接的,并且正在組成BuildContexts樹(父子關系)。
如果我們現在嘗試在上圖中說明BuildContext的概念,我們可以看到(仍然是一個非常簡化的視圖),其中每種顏色代表一個BuildContext(除了MyApp,它是不同的):
[站外圖片上傳中...(image-3be46-1539427274357)]
BuildContext可見性(簡化語句):
“ Something ”僅在其自己的BuildContext或其父BuildContext的BuildContext中可見。
從這個語句我們可以從子BuildContext派生出來,很容易找到一個祖先(= parent)Widget。
一個例子是,考慮Scaffold> Center> Column> Text:
context.ancestorWidgetOfExactType(Scaffold)=>通過從Text上下文轉到樹結構來返回第一個Scaffold。
從父BuildContext,也可以找到一個后代(=子)Widget,但不建議這樣做(我們稍后會討論)
小部件的類型
小部件有兩種類型:
無狀態小工具 Stateless Widget
這些可視組件中的一些除了它們自己的配置信息之外不依賴于任何其他信息,該信息在其直接父級構建時提供。
換句話說,這些小部件一旦創建就不必關心任何變化。
這些小部件稱為無狀態小部件。
這種小部件的典型示例可以是Text,Row,Column,Container ......其中,在構建時,我們只是將一些參數傳遞給它們。
參數可以是裝飾,尺寸甚至其他小部件中的任何內容。不要緊。唯一重要的是這個配置一旦應用,在下一個構建過程之前不會改變。
無狀態窗口小部件只能在加載/構建窗口小部件時繪制一次,這意味著無法基于任何事件或用戶操作重繪窗口小部件。
無狀態小部件生命周期
以下是與無狀態小組件相關的代碼的典型結構。
如您所見,我們可以將一些額外的參數傳遞給它的構造函數。但是,請記住,這些參數不會在稍后階段發生變化(變異),只能按原樣使用
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context){
return new ...
}
}
即使有另一種方法可以被重寫(createElement),后者幾乎從不被重寫。
唯一需要被重寫的是build
。
這種無狀態小部件的生命周期很簡單:
- 初始化
- 通過build()渲染
有狀態的小工具 Stateful Widget
其他一些小部件將處理一些在Widget生命周期內會發生變化的內部數據。因此,該數據變得動態。
此Widget保存的數據集可能會在此Widget的生命周期內發生變化,稱為State。
這些窗口小部件稱為有狀態窗口小部件(Stateful Widget)。
這樣的Widget的示例可以是用戶可以選擇的復選框列表或者根據條件禁用的Button。
State的概念
State定義StatefulWidget實例的“行為”部分。
它包含旨在與Widget交互/干擾的信息:
- 行為
- 布局
應用于狀態的任何更改都會強制Widget重建。
State和BuildContext之間的關系
對于有狀態窗口小部件,狀態與BuildContext關聯。
此關聯是永久性的 ,State對象永遠不會更改其BuildContext。
即使可以在樹結構周圍移動Widget BuildContext,State仍將與該BuildContext保持關聯。
當State與BuildContext關聯時,State被視為已掛載。
重點:
由于State對象與BuildContext相關聯,這意味著State對象不能(直接)通過另一個BuildContext訪問?。ㄎ覀儗⒃谏院笥懻撨@個問題)。
有狀態的小部件Stateful Widget 的生命周期
這是與Stateful Widget相關的典型代碼結構。
由于本文的主要目的是用“變量”數據來解釋State的概念,我將故意跳過與某些Stateful Widget overridable方法相關的任何解釋,這些方法與此沒有特別的關系。
這些可覆蓋的方法是didUpdateWidget,deactivate,reassemble
。這些將在另一篇文章中討論。
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.parameter,
}): super(key: key);
final parameter;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState(){
super.initState();
// Additional initialization of the State
}
@override
void didChangeDependencies(){
super.didChangeDependencies();
// Additional code
}
@override
void dispose(){
// Additional disposal code
super.dispose();
}
@override
Widget build(BuildContext context){
return new ...
}
}
下圖顯示了與創建有狀態窗口小部件相關的操作/調用序列(簡化版本)。在圖的右側,您將注意到流中的State對象的內部狀態。您還將看到上下文與狀態關聯的時刻,從而變為可用(已安裝)。
[站外圖片上傳中...(image-6003dd-1539427274357)]
所以讓我們用一些額外的細節來解釋它:
initState()
initState()方法是創建State對象后要調用的第一個方法(在構造函數之后)。
需要執行其他初始化時,將覆蓋重寫此方法。典型的初始化與動畫,控制器有關......
如果重寫此方法,則需要在第一個位置調用super.initState()方法。
在這個方法中,上下文context
可用,但你還不能真正使用它,因為框架還沒有完全將狀態與它相關聯。
initState()方法完成后,State對象現在已初始化,上下文可用。
在此State對象的生命周期內不再調用此方法。
didChangeDependencies()
didChangeDependencies()
方法是要調用的第二個方法。
在此階段,由于上下文可用,您可以使用它。
如果您的Widget鏈接到InheritedWidget和/或您需要初始化一些偵聽器(基于BuildContext),則通常會覆蓋此方法。
請注意,如果您的窗口小部件鏈接到InheritedWidget,則每次重建此窗口小部件時都會調用此方法。
如果重寫此方法,則應首先調用super.didChangeDependencies()
。
build()
build(BuildContext context)
方法在didChangeDependencies()
(和didUpdateWidget
)之后調用。
這是您構建窗口小部件(可能還有任何子樹)的位置。
每次State對象更改時(或者當InheritedWidget需要通知“已注冊”的小部件時)都會調用此方法!
為了強制重建,您可以調用setState((){...})
方法。
dispose()
放棄窗口小部件時調用dispose()方法。
如果你需要執行一些清理(例如監聽器,控制器......),然后立即調用super.dispose()
,則覆蓋此方法。
選擇無狀態還是有狀態小部件?
這是許多開發人員需要問自己的問題:“我是否需要我的Widget無狀態或有狀態?”
為了回答這個問題,請問自己:
在我的小部件的生命周期中,我是否需要考慮一個將要更改的變量,何時更改,將強制重建小部件?
如果問題的答案是肯定的,那么您需要一個有狀態的小部件,否則,您需要一個無狀態小部件。
一些例子:
- 用于顯示復選框列表的小組件。要顯示復選框,您需要考慮一系列項目。每個項目都是一個具有標題和狀態的對象。如果單擊復選框,則切換相應的
item.status
;在這種情況下,您需要使用有狀態窗口小部件來記住項目的狀態,以便能夠重繪復選框。 - 帶有表格的屏幕。該屏幕允許用戶填寫表單的窗口小部件并將表單發送到服務器。在這種情況下,在這種情況下,除非您在提交表單之前需要驗證表單或執行任何其他操作,否則無狀態窗口小部件可能就足夠了。
Stateful Widget由2部分組成
還記得Stateful小部件的結構嗎?有兩個部分:
Widget的主要定義
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
第一部分“MyStatefulWidget”通常是Widget的公共部分。當您要將其添加到窗口小部件樹時,可以實例化此部件。
此部分在Widget的生命周期內不會發生變化,但可能接受可由其相應的State實例使用的參數。
請注意,在Widget的第一部分定義的任何變量通常在其生命周期內不會更改。
Widget State定義
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
第二部分“_MyStatefulWidgetState”是在Widget的生命周期中變化的部分,并強制每次應用修改時重建Widget的這個特定實例。
名稱以_
開頭的字符使其成為.dart文件的私有。
如果需要在.dart文件之外引用此類,請不要使用“_”前綴。
_MyStatefulWidgetState類可以訪問存儲在MyStatefulWidget中的任何變量,使用widget.{變量的名稱}
。
例如:widget.color
小部件唯一標識 - key
在Flutter中,每個Widget都是唯一標識的。這個唯一標識由構建/渲染時的框架定義。
此唯一標識對應于可選的Key參數。如果省略,Flutter將為您生成一個。
在某些情況下,您可能需要強制使用此密鑰,以便可以通過其密鑰訪問窗口小部件。
為此,您可以使用以下幫助程序之一:GlobalKey <T>,LocalKey,UniqueKey 或ObjectKey。
該GlobalKey確保關鍵是在整個應用程序唯一的。
強制使用Widget的唯一標識:
GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
第2部分:如何進入State?
如前所述,State鏈接到一個BuildContext,BuildContext鏈接到Widget的一個實例。
1. Widget本身
理論上,唯一能夠訪問狀態的是Widget State本身。
在這種情況下,沒有困難。Widget State類訪問其任何變量。
2. 一個直接的 child Widget
有時,父窗口小部件可能需要訪問其直接子節點的狀態才能執行特定任務。
在這種情況下,要訪問這些直接子項State,您需要了解它們。
給某人打電話的最簡單方法是通過一個名字。在Flutter中,每個Widget都有一個唯一的標識,由框架在構建/渲染時確定。如前所示,您可以使用key參數強制使用Widget的標識。
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = new GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return new MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
一旦確定,父Widget可以通過以下方式訪問其子級的狀態:
myWidgetStateKey.currentState
讓我們考慮一個基本示例,當用戶點擊按鈕時顯示SnackBar。
由于SnackBar是Scaffold的子Widget,它不能直接被Scaffold身體的任何其他孩子訪問(還記得上下文的概念及其層次結構/樹結構嗎?)。因此,訪問它的唯一方法是通過ScaffoldState,它公開一個公共方法來顯示SnackBar。
class _MyScreenState extends State<MyScreen> {
/// the unique identity of the Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context){
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('My Screen'),
),
body: new Center(
new RaiseButton(
child: new Text('Hit me'),
onPressed: (){
_scaffoldKey.currentState.showSnackBar(
new SnackBar(
content: new Text('This is the Snackbar...'),
)
);
}
),
),
);
}
}
3. 祖先Widget
假設您有一個屬于另一個Widget的子樹的Widget,如下圖所示。
[站外圖片上傳中...(image-65249f-1539427274357)]
為了實現這一目標,需要滿足3個條件:
1.“帶狀態的小工具”(紅色)需要暴露其State
為了公開它的狀態,Widget需要在創建時記錄它,如下所示:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
return myState;
}
}
2.“Widget State”需要暴露一些getter / setter
為了讓“stranger”設置/獲取狀態屬性,Widget State需要通過以下方式授權訪問:
- 公共屬性 (不推薦)
- getter / setter
例如:
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
3.“想要獲得State的Widget”(上圖中藍色的widget)需要引用State
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
final MyExposingWidgetState state = widget?.myState;
return new Container(
color: state == null ? Colors.blue : state.color,
);
}
}
這個解決方案很容易實現,但子窗口小部件如何知道它何時需要重建?
在這個解決方案,它不知道。它必須等待重建才能刷新其內容,這不是很方便。
下一節將討論Inherited Widget的概念,它可以解決這個問題。
InheritedWidget
簡而言之,InheritedWidget允許在窗口小部件樹中有效地傳播(和共享)信息。
InheritedWidget是一個特殊的Widget,您可以將其作為另一個子樹的父級放在Widgets樹中。該子樹的所有小部件都必須能夠與該InheritedWidget公開的數據進行交互。
為了解釋它,讓我們看下代碼:
class MyInheritedWidget extends InheritedWidget {
MyInheritedWidget({
Key key,
@required Widget child,
this.data,
}): super(key: key, child: child);
final data;
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}
此代碼定義了一個名為“MyInheritedWidget”的Widget,旨在“共享”所有小部件(與子樹的一部分)中的某些數據。
如前所述,為了能夠傳播/共享一些數據,需要將InheritedWidget定位在窗口小部件樹的頂部,這解釋了傳遞給InheritedWidget基礎構造函數的“@required Widget child”。
“static MyInheritedWidget(BuildContext context)”方法允許所有子窗口小部件獲取最接近上下文的MyInheritedWidget的實例(參見后面)
最后,“updateShouldNotify”重寫方法用于告訴InheritedWidget是否必須將通知傳遞給所有子窗口小部件(已注冊/已訂閱),如果對數據應用了修改(請參閱下文)。
因此,我們需要將它放在樹節點級別,如下所示:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: new Row(
children: <Widget>[
...
],
),
);
}
}
子child如何訪問InheritedWidget的數據?
在構建子child時,后者將獲得對InheritedWidget的引用,如下所示:
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
/// 從此刻開始,窗口小部件可以使用MyInheritedWidget公開的數據
/// 通過調用:inheritedWidget.data
return new Container(
color: inheritedWidget.data.color,
);
}
}
如何在小部件之間進行交互?
請考慮以下顯示窗口小部件樹結構的圖表。
[站外圖片上傳中...(image-42aa42-1539427274357)]
為了說明一種交互方式,我們假設如下:
- '小部件A'是一個將項目添加到購物車的按鈕;
- “小部件B”是一個顯示購物車中商品數量的文本;
- “小部件C”位于小部件B旁邊,是一個內部帶有任何文本的文本;
- 我們希望“Widget B”在按下“Widget A”時自動在購物車中顯示正確數量的項目,但我們不希望重建“Widget C”
InheritedWidget就是用來干這個的Widget!
代碼示例我們先寫下代碼,然后解釋如下:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
}
class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => new MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
}
class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add(new Item(reference));
});
}
@override
Widget build(BuildContext context){
return new _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
new WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C');
}
}
說明
在這個非常基本的例子中,
- _MyInherited是一個InheritedWidget,每次我們通過點擊“Widget A”按鈕添加一個Item時都會重新創建它
- MyInheritedWidget是一個Widget,其狀態包含Items列表。可以通過“(BuildContext context)的靜態MyInheritedWidgetState”訪問此狀態。
- MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便它們可以被小部件使用,這是子小部件樹的一部分
- 每次我們向State添加一個Item時,MyInheritedWidgetState都會重建
- MyTree類只是構建一個小部件樹,將MyInheritedWidget作為樹的父級
- WidgetA是一個簡單的RaisedButton,當按下它時,從最近的MyInheritedWidget調用addItem方法
- WidgetB是一個簡單的文本,顯示最接近的MyInheritedWidget級別的項目數
這一切如何運作?
注冊Widget以供以后通知
當子Widget調用MyInheritedWidget.of(context)時,它會調用MyInheritedWidget的以下方法,并傳遞自己的BuildContext。
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
在內部,除了簡單地返回MyInheritedWidgetState的實例之外,它還將消費者窗口小部件訂閱到更改通知。
在場景后面,對這個靜態方法的簡單調用實際上做了兩件事:
- 當對InheritedWidget應用修改時,“consumer”窗口小部件會自動添加到將重建的訂戶列表中(此處為_MyInherited)
- _MyInherited小部件(又名MyInheritedWidgetState)中引用的數據將返回給“使用者”
過程
由于'Widget A'和'Widget B'都已使用InheritedWidget訂閱,因此如果對_MyInherited應用了修改,則當單擊Widget A的RaisedButton時,操作流程如下(簡化版本):
- 調用MyInheritedWidgetState的addItem方法
- MyInheritedWidgetState.addItem方法將新項添加到List <Item>
- 調用setState()以重建MyInheritedWidget
- 使用List <Item>的新內容創建_MyInherited的新實例
- _MyInherited記錄在參數(數據)中傳遞的新State作為InheritedWidget,它檢查是否需要“通知”“使用者”(答案為是)
- 它迭代整個消費者列表(這里是Widget A和Widget B)并請求他們重建
- 由于Wiget C不是消費者,因此不會重建。
嗯,就是這么干的 !
但是,Widget A和Widget B都重建了,而重建Wiget A沒用,因為它沒有任何改變。如何防止這種情況發生?
在仍然訪問“繼承的”小組件時阻止某些小組件重建
Widget A也被重建的原因來自它訪問MyInheritedWidgetState的方式。
正如我們之前看到的,調用context.inheritFromWidgetOfExactType()
方法的實際上是自動將Widget訂閱到“使用者”列表。
防止此自動訂閱同時仍允許Widget A訪問MyInheritedWidgetState的解決方案是更改MyInheritedWidget的靜態方法,如下所示:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
通過添加布爾類型的額外參數...
- 如果“rebuild”參數為true(默認情況下),我們使用常規方法(并且Widget將添加到訂閱者列表中)
- 如果“rebuild”參數為false,我們仍然可以訪問數據,但不使用InheritedWidget的內部實現
因此,要完成解決方案,我們還需要稍微更新Widget A的代碼,如下所示(我們添加false額外參數):
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
在那里,當我們按下它時,Widget A不再重建。
Routes, Dialogs的特別說明......
路由Routes,對話框Dialogs , BuildContexts與應用程序綁定。這意味著即使在屏幕A內部您要求顯示另一個屏幕B(例如,在當前的屏幕上),兩個屏幕中的任何一個都沒有“簡單的方法”來關聯它們自己的上下文。屏幕B了解屏幕A上下文的唯一方法是從屏幕A獲取它作為Navigator.of(context).push(...。)的參數。
推薦閱讀:
[1] : flutter屏幕適配
[2] : Maksim Ryzhikov
[3] : Chema Molins
[4] : Official documentation
[5] : Video from Google I/O 2018
[6] : Scoped_Model
結論
關于這些主題還有很多話要說......特別是在InheritedWidget上。
其他感興趣的主題是Notifiers / Listeners 以及(可能主要)Streams的概念,但這將在其他文章中介紹。
感謝您閱讀這篇相當長的文章,請繼續關注下一個快樂的編碼......