## 計數器應用示例
用Android Studio和VS Code創建的Flutter應用模板是一個簡單的計數器示例,本節先仔細講解一下這個計數器Demo的源碼,讓讀者對Flutter應用程序結構有個基本了解,在隨后小節中,將會基于此示例,一步一步添加一些新的功能來介紹Flutter應用的其它概念與技術。對于接下來的示例,希望讀者可以跟著筆者實際動手來寫一下,這樣不僅可以加深印象,而且也會對介紹的概念與技術有一個真切的體會。如果你還不是很熟悉Dart或者沒有移動開發經驗,不用擔心,只要你熟悉面向對象和基本編程概念(如變量、循環和條件控制),則可以完成本示例。
通過Android Studio和VS Code根據前面“編輯器配置與使用”一章中介紹的創建Flutter工程的方法創建一個新的Flutter工程,命名為"first\_flutter\_app"。創建好后,就會得到一個計數器應用的Demo。
> 注意,默認Demo示例可能隨著編輯器Flutter插件版本變化而變化,本例中會介紹計數器示例的全部代碼,所以不會對本示例產生影響。
我們先運行此示例,效果圖如下:

該計數器示例中,每點擊一次右下角帶“?”號的懸浮按鈕,屏幕中央的數字就會加1。
在這個示例中,主要Dart代碼是在 **lib/main.dart** 文件中,下面我們看看該示例的源碼:
```
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
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),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
```
### 分析
1. 導入包。
```
import 'package:flutter/material.dart';
```
此行代碼作用是導入了Material UI組件庫。[Material](https://material.io/guidelines/)是一種標準的移動端和web端的視覺設計語言, Flutter默認提供了一套豐富的Material風格的UI組件。
2. 應用入口。
```
void main() => runApp(new MyApp());
```
- 與C/C++、Java類似,Flutter 應用中`main`函數為應用程序的入口,main函數中調用了,`runApp` 方法,它的功能是啟動Flutter應用,它接受一個`Widget`參數,在本示例中它是`MyApp`類的一個實例,該參數代表Flutter應用。
- main函數使用了(`=>`)符號,這是Dart中單行函數或方法的簡寫。
3. 應用結構。
```
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
//應用名稱
title: 'Flutter Demo',
theme: new ThemeData(
//藍色主題
primarySwatch: Colors.blue,
),
//應用首頁路由
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
```
- `MyApp`類代表Flutter應用,它繼承了 `StatelessWidget`類,這也就意味著應用本身也是一個widget。
- 在Flutter中,大多數東西都是widget,包括對齊(alignment)、填充(padding)和布局(layout)。
- Flutter在構建頁面時,會調用組件的`build`方法,widget的主要工作是提供一個build()方法來描述如何構建UI界面(通常是通過組合、拼裝其它基礎widget)。
- `MaterialApp` 是Material庫中提供的Flutter APP框架,通過它可以設置應用的名稱、主題、語言、首頁及路由列表等。`MaterialApp`也是一個widget。
- `Scaffold` 是Material庫中提供的頁面腳手架,它包含導航欄和Body以及FloatingActionButton(如果需要的話)。 本書后面示例中,路由默認都是通過`Scaffold`創建。
- `home` 為Flutter應用的首頁,它也是一個widget。
4. 首頁
```
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
...
}
```
`MyHomePage` 是應用的首頁,它繼承自`StatefulWidget`類,表示它是一個有狀態的widget(Stateful widget)。現在,我們可以簡單認為Stateful widget 和Stateless widget有兩點不同:
1. Stateful widget可以擁有狀態,這些狀態在widget生命周期中是可以變的,而Stateless widget是不可變的。
2. Stateful widget至少由兩個類組成:
- 一個`StatefulWidget`類。
- 一個 State類; `StatefulWidget`類本身是不變的,但是 State類中持有的狀態在widget生命周期中可能會發生變化。
`_MyHomePageState`類是`MyHomePage`類對應的狀態類。看到這里,細心的讀者可能已經發現,和`MyApp` 類不同, `MyHomePage`類中并沒有`build`方法,取而代之的是,`build`方法被挪到了`_MyHomePageState`方法中,至于為什么這么做,先留個疑問,在分析完完整代碼后再來解答。
接下來,我們看看`_MyHomePageState`中都包含哪些東西:
1. 狀態。
```
int _counter = 0;
```
`_counter` 為保存屏幕右下角帶“?”號按鈕點擊次數的狀態。
2. 設置狀態的自增函數。
```
void _incrementCounter() {
setState(() {
_counter++;
});
}
```
當按鈕點擊時,會調用此函數,該函數的作用是先自增`_counter`,然后調用`setState` 方法。`setState`方法的作用是通知Flutter框架,有狀態發生了改變,Flutter框架收到通知后,會執行`build`方法來根據新的狀態重新構建界面, Flutter 對此方法做了優化,使重新執行變的很快,所以你可以重新構建任何需要更新的東西,而無需分別去修改各個widget。
3. 構建UI界面
構建UI界面的邏輯在`build`方法中,當`MyHomePage`第一次創建時,`_MyHomePageState`類會被創建,當初始化完成后,Flutter框架會調用Widget的`build`方法來構建widget樹,最終將widget樹渲染到設備屏幕上。所以,我們看看`_MyHomePageState`的`build`方法中都干了什么事:
```
Widget build(BuildContext context) {
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),
),
);
}
```
- Scaffold 是 Material庫中提供的一個widget, 它提供了默認的導航欄、標題和包含主屏幕widget樹的body屬性。widget樹可以很復雜。
- body的widget樹中包含了一個`Center` widget,`Center` 可以將其子widget樹對齊到屏幕中心, `Center` 子widget是一個`Column` widget,`Column`的作用是將其所有子widget沿屏幕垂直方向依次排列, 此例中`Column`包含兩個 `Text`子widget,第一個`Text` widget顯示固定文本 “You have pushed the button this many times:”,第二個`Text` widget顯示`_counter`狀態的數值。
- floatingActionButton是頁面右下角的帶“?”的懸浮按鈕,它的`onPressed`屬性接受一個回調函數,代表它本點擊后的處理器,本例中直接將`_incrementCounter`作為其處理函數。
現在,我們將整個流程串起來:當右下角的floatingActionButton按鈕被點擊之后,會調用`_incrementCounter`,在`_incrementCounter`中,首先會自增`_counter`計數器(狀態),然后`setState`會通知Flutter框架狀態發生變化,接著,Flutter會調用`build`方法以新的狀態重新構建UI,最終顯示在設備屏幕上。
#### 為什么要將build方法放在State中,而不是放在StatefulWidget中?
現在,我們回答之前提出的問題,為什么`build()`方法在State(而不是StatefulWidget)中 ?這主要是為了開發的靈活性。如果將`build()`方法在StatefulWidget中則會有兩個問題:
- 狀態訪問不便
試想一下,如果我們的Stateful widget 有很多狀態,而每次狀態改變都要調用`build`方法,由于狀態是保存在State中的,如果將`build`方法放在StatefulWidget中,那么構建時讀取狀態將會很不方便,試想一下,如果真的將`build`方法放在StatefulWidget中的話,由于構建用戶界面過程需要依賴State,所以`build`方法將必須加一個`State`參數,大概是下面這樣:
```
Widget build(BuildContext context, State state){
//state.counter
...
}
```
這樣的話就只能將State的所有狀態聲明為公開的狀態,這樣才能在State類外部訪問狀態,但將狀態設置為公開后,狀態將不再具有私密性,這樣依賴,對狀態的修改將會變的不可控。將`build()`方法放在State中的話,構建過程則可以直接訪問狀態,這樣會很方便。
- 繼承StatefulWidget不便
例如,Flutter中有一個動畫widget的基類`AnimatedWidget`,它繼承自`StatefulWidget`類。`AnimatedWidget`中引入了一個抽象方法`build(BuildContext context)`,繼承自`AnimatedWidget`的動畫widget都要實現這個`build`方法。現在設想一下,如果`StatefulWidget` 類中已經有了一個`build`方法,正如上面所述,此時`build`方法需要接收一個state對象,這就意味著`AnimatedWidget`必須將自己的State對象(記為\_animatedWidgetState)提供給其子類,因為子類需要在其`build`方法中調用父類的`build`方法,代碼可能如下:
```
class MyAnimationWidget extends AnimatedWidget{
@override
Widget build(BuildContext context, State state){
//由于子類要用到AnimatedWidget的狀態對象_animatedWidgetState,
//所以AnimatedWidget必須通過某種方式將其狀態對象_animatedWidgetState
//暴露給其子類
super.build(context, _animatedWidgetState)
}
}
```
這樣很顯然是不合理的,因為
1. `AnimatedWidget`的狀態對象是`AnimatedWidget`內部實現細節,不應該暴露給外部。
2. 如果要將父類狀態暴露給子類,那么必須得有一種傳遞機制,而做這一套傳遞機制是無意義的,因為父子類之間狀態的傳遞和子類本身邏輯是無關的。
綜上所述,可以發現,對于StatefulWidget,將`build`方法放在State中,可以給開發帶來很大的靈活性。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示