在專欄的第一篇預習文章中,我和你一起搭建了 Flutter 的開發環境,并且通過自帶的 hello\_world 示例,和你演示了 Flutter 項目是如何運行在 Android 和 iOS 模擬器以及真機上的。
今天,我會通過 Android Studio 創建的 Flutter 應用模板,帶你去了解 Flutter 的項目結構,分析 Flutter 工程與原生 Android 和 iOS 工程有哪些聯系,體驗一個有著基本功能的 Flutter 應用是如何運轉的,從而加深你對構建 Flutter 應用的關鍵概念和技術的理解。
如果你現在還不熟悉 Dart 語言也不用擔心,只要能夠理解基本的編程概念(比如,類型、變量、函數和面向對象),并具備一定的前端基礎(比如,了解 View 是什么、頁面基本布局等基礎知識),就可以和我一起完成今天的學習。而關于 Dart 語言基礎概念的講述、案例分析,我會在下一個模塊和你展開。
## 計數器示例工程分析
首先,我們打開 Android Studio,創建一個 Flutter 工程應用 flutter\_app。Flutter 會根據自帶的應用模板,自動生成一個簡單的計數器示例應用 Demo。我們先運行此示例,效果如下:
:-: 
圖 1 計數器示例運行效果
每點擊一次右下角帶“+”號的懸浮按鈕,就可以看到屏幕中央的數字隨之 +1。
### 工程結構
在體會了示例工程的運行效果之后,我們再來看看 Flutter 工程目錄結構,了解 Flutter 工程與原生 Android 和 iOS 工程之間的關系,以及這些關系是如何確保一個 Flutter 程序可以最終運行在 Android 和 iOS 系統上的。
:-: 
圖 2 Flutter 工程目錄結構
可以看到,除了 Flutter 本身的代碼、資源、依賴和配置之外,Flutter 工程還包含了 Android 和 iOS 的工程目錄。
這也不難理解,因為 Flutter 雖然是跨平臺開發方案,但卻需要一個容器最終運行到 Android 和 iOS 平臺上,所以**Flutter 工程實際上就是一個同時內嵌了 Android 和 iOS 原生子工程的父工程**:我們在 lib 目錄下進行 Flutter 代碼的開發,而某些特殊場景下的原生功能,則在對應的 Android 和 iOS 工程中提供相應的代碼實現,供對應的 Flutter 代碼引用。
Flutter 會將相關的依賴和構建產物注入這兩個子工程,最終集成到各自的項目中。而我們開發的 Flutter 代碼,最終則會以原生工程的形式運行。
### 工程代碼
在對 Flutter 的工程結構有了初步印象之后,我們就可以開始學習 Flutter 的項目代碼了。
Flutter 自帶的應用模板,也就是這個計數器示例,對初學者來說是一個極好的入門范例。在這個簡單示例中,從基礎的組件、布局到手勢的監聽,再到狀態的改變,Flutter 最核心的思想在這 60 余行代碼中展現得可謂淋漓盡致。
為了便于你學習理解,領會構建 Flutter 程序的大體思路與關鍵技術,而不是在一開始時就陷入組件的 API 細節中,我刪掉了與核心流程無關的組件配置代碼及布局邏輯,在不影響示例功能的情況下對代碼進行了改寫,并將其分為兩部分:
* 第一部分是應用入口、應用結構以及頁面結構,可以幫助你理解構建 Flutter 程序的基本結構和套路;
* 第二部分則是頁面布局、交互邏輯及狀態管理,能夠幫你理解 Flutter 頁面是如何構建、如何響應交互,以及如何更新的。
首先,我們來看看**第一部分的代碼**,也就是應用的整體結構:
```
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(home: MyHomePage(title: 'Flutter Demo Home Page'));
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) => {...};
}
```
在本例中,Flutter 應用為 MyApp 類的一個實例,而 MyApp 類繼承自 StatelessWidget 類,這也就意味著應用本身也是一個 Widget。事實上,在 Flutter 中,Widget 是整個視圖描述的基礎,在 Flutter 的世界里,包括應用、視圖、視圖控制器、布局等在內的概念,都建立在 Widget 之上,**Flutter 的核心設計思想便是一切皆 Widget**。
Widget 是組件視覺效果的封裝,是 UI 界面的載體,因此我們還需要為它提供一個方法,來告訴 Flutter 框架如何構建 UI 界面,這個方法就是 build。
在 build 方法中,我們通常通過對基礎 Widget 進行相應的 UI 配置,或是組合各類基礎 Widget 的方式進行 UI 的定制化。比如在 MyApp 中,我通過 MaterialApp 這個 Flutter App 框架設置了應用首頁,即 MyHomePage。當然,MaterialApp 也是一個 Widget。
MaterialApp 類是對構建 material 設計風格應用的組件封裝框架,里面還有很多可配置的屬性,比如應用主題、應用名稱、語言標識符、組件路由等。但是,這些配置屬性并不是本次分享的重點,如果你感興趣的話,可以參考 Flutter 官方的[API 文檔](https://api.flutter.dev/flutter/material/MaterialApp/MaterialApp.html),來了解 MaterialApp 框架的其他配置能力。
MyHomePage 是應用的首頁,繼承自 StatefulWidget 類。這,代表著它是一個有狀態的 Widget(Stateful Widget),而 \_MyHomePageState 就是它的狀態。
如果你足夠細心的話就會發現,雖然 MyHomePage 類也是 Widget,但與 MyApp 類不同的是,它并沒有一個 build 方法去返回 Widget,而是多了一個 createState 方法返回 \_MyHomePageState 對象,而 build 方法則包含在這個 \_MyHomePageState 類當中。
那么,**StatefulWidget 與 StatelessWidget 的接口設計,為什么會有這樣的區別呢?**
這是因為 Widget 需要依據數據才能完成構建,而對于 StatefulWidget 來說,其依賴的數據在 Widget 生命周期中可能會頻繁地發生變化。由 State 創建 Widget,以數據驅動視圖更新,而不是直接操作 UI 更新視覺屬性,代碼表達可以更精煉,邏輯也可以更清晰。
在了解了計數器示例程序的整體結構以后,我們再來看看這個**示例代碼的第二部分**,也就是頁面布局及交互邏輯部分。
```
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() => setState(() {_counter++;});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Widget.title)),
body: Text('You have pushed the button this many times:$_counter')),
floatingActionButton: FloatingActionButton(onPressed: _incrementCounter)
);
}
```
\_MyHomePageState 中創建的 Widget Scaffold,是 Material 庫中提供的頁面布局結構,它包含 AppBar、Body,以及 FloatingActionButton。
* AppBar 是頁面的導航欄,我們直接將 MyHomePage 中的 title 屬性作為標題使用。
* body 則是一個 Text 組件,顯示了一個根據 \_counter 屬性可變的文本:‘You have pushed the button this many times:$\_counter’。
* floatingActionButton,則是頁面右下角的帶“+”的懸浮按鈕。我們將 \_incrementCounter 作為其點擊處理函數。
\_incrementCounter 的實現很簡單,使用 setState 方法去自增狀態屬性 \_counter。setState 方法是 Flutter 以數據驅動視圖更新的關鍵函數,它會通知 Flutter 框架:我這兒有狀態發生了改變,趕緊給我刷新界面吧。而 Flutter 框架收到通知后,會執行 Widget 的 build 方法,根據新的狀態重新構建界面。
**這里需要注意的是:狀態的更改一定要配合使用 setState。**通過這個方法的調用,Flutter 會在底層標記 Widget 的狀態,隨后觸發重建。于我們的示例而言,即使你修改了 \_counter,如果不調用 setState,Flutter 框架也不會感知到狀態的變化,因此界面上也不會有任何改變(你可以動手驗證一下)。
下面的圖 3,就是整個計數器示例的代碼流程示意圖。通過這張圖,你就能夠把這個實例的整個代碼流程串起來了:
:-: 
圖 3 代碼流程示意圖
MyApp 為 Flutter 應用的運行實例,通過在 main 函數中調用 runApp 函數實現程序的入口。而應用的首頁則為 MyHomePage,一個擁有 \_MyHomePageState 狀態的 StatefulWidget。\_MyHomePageState 通過調用 build 方法,以相應的數據配置完成了包括導航欄、文本及按鈕的頁面視圖的創建。
而當按鈕被點擊之后,其關聯的控件函數 \_incrementCounter 會觸發調用。在這個函數中,通過調用 setState 方法,更新 \_counter 屬性的同時,也會通知 Flutter 框架其狀態發生變化。隨后,Flutter 會重新調用 build 方法,以新的數據配置重新構建 \_MyHomePageState 的 UI,最終完成頁面的重新渲染。
Widget 只是視圖的“配置信息”,是數據的映射,是“只讀”的。對于 StatefulWidget 而言,當數據改變的時候,我們需要重新創建 Widget 去更新界面,這也就意味著 Widget 的創建銷毀會非常頻繁。
為此,Flutter 對這個機制做了優化,其框架內部會通過一個中間層去收斂上層 UI 配置對底層真實渲染的改動,從而最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是上層 UI 配置變了就需要銷毀整個渲染視圖樹重建。
這樣一來,Widget 僅是一個輕量級的數據配置存儲結構,它的重新創建速度非常快,所以我們可以放心地重新構建任何需要更新的視圖,而無需分別修改各個子 Widget 的特定樣式。關于 Widget 具體的渲染過程細節,我會在后續的第 9 篇文章“Widget,構建 Flutter 界面的基石”中向你詳細介紹,在這里就不再展開了。
## 總結
今天的這次 Flutter 項目初體驗,我們就先進行到這里。接下來,我們一起回顧下涉及到的知識點。
首先,我們通過 Flutter 標準模板創建了計數器示例,并分析了 Flutter 的項目結構,以及 Flutter 工程與原生 Android、iOS 工程的聯系,知道了 Flutter 代碼是怎么運行在原生系統上的。
然后,我帶你學習了示例項目代碼,了解了 Flutter 應用結構及頁面結構,并認識了構建 Flutter 的基礎,也就是 Widget,以及狀態管理機制,知道了 Flutter 頁面是如何構建的,StatelessWidget 與 StatefulWidget 的區別,以及如何通過 State 的成員函數 setState 以數據驅動的方式更新狀態,從而更新頁面。
有原生 Android 和 iOS 框架開發經驗的同學,可能更習慣命令式的 UI 編程風格:手動創建 UI 組件,在需要更改 UI 時調用其方法修改視覺屬性。而 Flutter 采用聲明式 UI 設計,我們只需要描述當前的 UI 狀態(即 State)即可,不同 UI 狀態的視覺變更由 Flutter 在底層完成。
雖然命令式的 UI 編程風格更直觀,但聲明式 UI 編程方式的好處是,可以讓我們把復雜的視圖操作細節交給框架去完成,這樣一來不僅可以提高我們的效率,也可以讓我們專注于整個應用和頁面的結構和功能。
所以在這里,我非常希望你能夠適應這樣的 UI 編程思維方式的轉換。
## 思考題
最后,我給你留下一個思考題吧。
示例項目代碼在 \_MyHomePageState 類中,直接在 build 函數里以內聯的方式完成了 Scaffold 頁面元素的構建,這樣做的好處是什么呢?
在實現同樣功能的情況下,如果將 Scaffold 頁面元素的構建封裝成一個新 Widget 類,我們該如何處理?
- 前言
- 開篇詞
- 預習篇
- 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混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略