入門: 在 macOS 上搭建 Flutter 開發環境
一、獲取Flutter SDK
去 flutter 官網下載其最新可用的安裝包:Flutter SDK releases,或者去 Flutter 的 Github 倉庫去下載:flutter/flutter。
二、更新環境變數
新增 flutter 相關工具到 path 中:
export PATH=`pwd`/flutter/bin:$PATH
複製程式碼
此程式碼只能暫時針對當前命令列視窗設定 PATH 環境變數,要想永久將 Flutter 新增到 PATH 中需要更新環境變數,以便你可以執行 flutter 命令在任何終端會話中。
- 開啟(或建立)
.bash_profile
,此檔案在不同的機器上可能檔案路徑不同,例如在我的電腦上在此路徑:/Users/hmc/.bash_profile
。如果.bash_profile
檔案不存在的話可以cd
到Users/xxx(你當前的使用者名稱)
路徑下,然後使用vim .bash_profile
命令自行建立一個。 - 往
.bash_profile
檔案中新增以下行並更改PATH_TO_FLUTTER_GIT_DIRECTORY
為下載 Flutter SDK 到本地的路徑,例如我的 Flutter SDK 的本地路徑是:/Users/hmc/Documents/GitHub/flutter
,下面示例中的第 3 行則修改為:export PATH=/Users/hmc/Documents/GitHub/flutter/bin:$PATH
。
export PUB_HOSTED_URL=https://pub.flutter-io.cn // 國內使用者需要設定
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn // 國內使用者需要設定
export PATH=PATH_TO_FLUTTER_GIT_DIRECTORY/flutter/bin:$PATH
複製程式碼
- 開啟一個終端視窗執行
source /Users/hmc/.bash_profile
命令進行重新整理(.bash_profile
檔案的實際路徑大家以自己的機器為準)。 - 如果終端使用的是 zsh,則終端啟動時
~/.bash_profile
檔案將不會被載入,解決辦法就是修改~/.zshrc
(我本機的路徑是:/Users/hmc/.zshrc
),在其中新增:source ~/.bash_profile
。如果.zshrc
檔案不存在的話,可使用如下命令建立:
touch .zshrc
open -e .zshrc
複製程式碼
然後在其中輸入:source ~/.bash_profile
並儲存,然後輸入 source .zshrc
命令重新整理環境使環境變數生效。
三、執行 flutter doctor
執行 flutter doctor
命令檢視是否需要安裝其它依賴項並完成 Flutter 環境的整體安裝。flutter doctor
命令檢查你的環境並在終端視窗中顯示報告,Dart SDK 已經捆綁在 Flutter 裡了,沒有必要單獨安裝 Dart。仔細檢查命令列輸出以獲取可能需要安裝的其他軟體或進一步需要執行的任務(以粗體顯示)。
如果大家當前已經有 iOS 開發環境的話,那麼配置到這裡,Flutter 環境基本就配置完成了,通過上述配置,我本機執行 flutter doctor
命令(此命令第一次執行會比較慢),輸出如下:
hmc@localhost ~ % flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel master, 2.5.0-7.0.pre.185, on macOS 11.4 20F5046g
darwin-x64, locale zh-Hans-CN)
[✗] Android toolchain - develop for Android devices
✗ Unable to locate Android SDK.
Install Android Studio from:
https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK
components.
(or visit https://flutter.dev/docs/get-started/install/macos#android-setup
for detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.
[✓] Xcode - develop for iOS and macOS (Xcode 12.4)
[✓] Chrome - develop for the web
[!] Android Studio (not installed)
[✓] VS Code (version 1.42.1)
[✓] Connected device (2 available)
! Doctor found issues in 2 categories.
hmc@localhost ~ %
複製程式碼
看到 android 環境還需要配置,後續如果需要我們再進行。至此Flutter 的 iOS 環境便已經配好了。
起步: 配置編輯器
一、安裝 Visual Studio Code
IDE 部分我們直接選擇 Visual Studio Code 需要 1.20.1或更高版本,當前 VS Code 版本已經到 1.59,下載安裝完 VS Code 以後我們需要安裝 Flutter 外掛。
二、安裝 Flutter 外掛
啟動 VS Code,shift + Command + p 調出 VS Code 的命令皮膚,輸入 install,然後選擇 Extensions: Install Extensions,在搜尋框裡輸入 flutter,在搜尋列表結果中選擇:Flutter Flutter support and debugger for Visual Studio Code. 並點選 install 按鈕,安裝完成選擇 OK 重新啟動 VS Code。
三、通過 Flutter Doctor 驗證設定
shift + Command + p 調出 VS Code 的命令皮膚,輸入 doctor,然後選擇:Flutter: Run Flutter Doctor,這個指令需要執行一小會,然後查詢輸出,它會詳細列出你的機器當前的 Flutter 版本、本地路徑、來源、引擎版本、Dart 版本、Xcode 版本及位置、CocoaPods 版本、Chrome 位置、VS Code 版本及位置、外掛版本、連線的裝置 等等的詳細資訊。我的機器如下(沒有安裝安卓環境)
[flutter] flutter doctor -v
[✓] Flutter (Channel master, 2.5.0-7.0.pre.185, on macOS 11.4 20F5046g darwin-x64, locale zh-Hans-CN)
• Flutter version 2.5.0-7.0.pre.185 at /Users/hmc/Documents/GitHub/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 2526cb07cb (2 days ago), 2021-08-21 23:42:01 -0400
• Engine revision 4783663ee4
• Dart version 2.15.0 (build 2.15.0-41.0.dev)
• Pub download mirror https://pub.flutter-io.cn
• Flutter download mirror https://storage.flutter-io.cn
[✗] Android toolchain - develop for Android devices
✗ Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.
[✓] Xcode - develop for iOS and macOS (Xcode 12.4)
• Xcode at /Applications/Xcode.app/Contents/Developer
• CocoaPods version 1.10.1
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[!] Android Studio (not installed)
• Android Studio not found; download from https://developer.android.com/studio/index.html
(or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions).
[✓] VS Code (version 1.42.1)
• VS Code at /Users/hmc/Downloads/Visual Studio Code-2.app/Contents
• Flutter extension version 3.8.1
[✓] Connected device (3 available)
• iPhone 12 (mobile) • FF9BFB96-8FF4-4AD6-98B8-1C8889653AF0 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-4 (simulator)
• iPhone 12 Pro (mobile) • CC2922E4-A2DB-43DF-8B6F-D2987F683525 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-4 (simulator)
• Chrome (web) • chrome • web-javascript • Google Chrome 92.0.4515.159
! Doctor found issues in 2 categories.
exit code 0
複製程式碼
至此 IDE 配置完成。
起步: 簡單體驗
本節從我們的模板建立一個新的 Flutter 應用程式,執行它,並學習如何使用 Hot Reload(熱過載) 進行更新過載。
一、建立 Flutter 新應用
shift + Command + p 調出 VS Code 的命令皮膚,輸入 flutter,然後選擇 Flutter: New Project,然後在輸入框中輸入專案名稱(如:FirstFlutterDemo),然後按Enter鍵,選擇專案的本地路徑,專案建立完成,VS Code 會預設選中並開啟 main.dart 檔案,確保在 VS Code 的右下角選擇了目標裝置,然後按 F5 鍵或呼叫 Debug > Start Debugging,等待應用程式啟動,如果一切正常,在應用程式建立啟動成功後,應該在裝置或模擬器上看到應用程式截圖如下:
二、熱過載
Flutter 可以通過 熱過載(hot reload) 實現快速的開發週期,熱過載就是無需重啟應用程式(原生開發,修改任意一行程式碼都要重新編譯執行才能生效)就能實時載入修改後的程式碼,並且不會丟失狀態。簡單的對程式碼進行更改,然後告訴 IDE 或命令列工具你需要重新載入(點選 Hot Reload 按鈕),你就會在你的裝置或模擬器上看到更改。如在 main.dart 檔案中修改 You have pushed the button this many times:
字串內容,然後點選 command + s 儲存,此時便立刻能在模擬器上看到更新的字串。
正式編寫第一個 Flutter App
新建一個命名為 startup_namer 的 Flutter 專案。直接刪除 lib/main.dart 中的全部程式碼,然後替換為如下程式碼並執行,它僅僅顯示一個標題是 Welcome to Flutter
的藍色導航條和螢幕中心的 Hello World
文字。
// 引入 material.dart
import 'package:flutter/material.dart';
// => 符號是 Dart 中單行函式/方法的簡寫,同 void main() { runApp(new MyApp()); }
// runApp 函式的引數是一個 MyApp 例項
void main() => runApp(new MyApp());
// MyApp 類繼承自 StatelessWidget
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
// Scaffold 是來自 Material 庫中
home: new Scaffold(
// 導航欄
appBar: new AppBar(
// 導航欄標題
title: new Text('Welcome to Flutter'),
),
// 主螢幕內容
body: new Center(
// 文字 widget
child: new Text('Hello World'),
),
),
);
}
}
複製程式碼
以上示例是建立了一個 Material APP。(Material 是一種標準的移動端和 web 端的視覺設計語言。 Flutter 提供了一套豐富的 Material widgets。)
MyApp
類繼承自 StatelessWidget
,這將會使應用本身也成為一個 widget。 在 Flutter 中,大多數東西都是 widget,包括對齊(alignment
)、填充(padding
)和佈局(layout
)。
Scaffold
是 Material library 庫中提供的一個 widget,它提供了預設的導航欄(appBar
)、標題(title
)和包含主螢幕 widget 樹的 body
屬性。widget 樹可以很複雜。widget 的主要工作是提供一個 build()
方法來描述如何根據其他較低階別的 widget 來顯示自己。
示例中的 body
的 widget 樹中包含了一個 Center
widget,Center
widget又包含一個 Text
widget 作為其子 widget。 Center
widget 可以將其子 widget 樹對齊到螢幕中心。
使用外部包(package)
在上面的例項中,我們使用了 flutter/material.dart
這個 預設的 package,下面我們嘗試匯入其他自定義的 package。(本節以引入 english_words 4.0.0 package 為例,english_words 是用於處理英語單詞的實用程式。計算音節,生成聽起來不錯的單片語合,並提供按用法排名前 5000 的英語單詞。)
在 pub.dartlang.org 中我們能看到有很多不同功能的 package,我們在其中搜尋到:english_words 4.0.0。
一、修改 pubspec.yaml 檔案指定要引入的 package
pubspec.yaml 檔案用來管理 Flutter 應用程式的 assets(資源,如圖片、package 等)。下面我們在 pubspec.yaml 檔案中,將 english_words: ^4.0.0
新增到 dependencies:
下面,作為當前程式的一個依賴項。當前 startup_namer
這個 Flutter 應用程式的 pubspec.yaml 檔案的完整內容如下:
name: startup_namer
description: A new Flutter project.
# 以下行可防止使用 `flutter pub publish` 意外地將 package 釋出到 pub.dev。這是 private packages 的首選項。
publish_to: 'none' # 如果你希望釋出到 pub.dev,請刪除此行
# 下面定義了應用程式的版本號和構建號。
# 版本號是按點分隔的三個數字,如 1.2.43,後跟可選的構建號用一個 + 分開。
# 版本和 builder 號都可以通過分別指定 --build-name 和 --build-number 來覆蓋在 flutter 構建中。
#
# 在 Android 中,build-name 用作 versionName,而 build-number 用作 versionCode。
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
#
# 在 iOS 中,build-name 用作 CFBundleShortVersionString,而 build-number 用作 CFBundleVersion。
# Read more about iOS versioning at https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
# dependencies 指定你的 package 需要的其他 packages 才能工作。要自動將你的 package dependencies 升級到最新版本,請考慮執行:flutter pub upgrade --major-versions 指令。
# 或者,可以通過將下面的版本編號更改為 pub.dev 上提供的最新版本來手動更新 dependencies。要檢視哪些 dependencies 有較新的版本可用,請執行:flutter pub outdated 指令。
dependencies:
flutter:
sdk: flutter
# 下面將 Cupertino Icons 字型新增到你的應用程式中。與 iOS 樣式圖示的 CupertinoIcons 類一起使用。
cupertino_icons: ^1.0.2
# 引入 english_words
english_words: ^4.0.0
dev_dependencies:
flutter_test:
sdk: flutter
# 下面的 "flutter_lints" package 包含一組推薦的 lints,以鼓勵良好的編碼實踐。
# package 提供的 lint set 在位於 package root 的 "analysis_options.yaml" 檔案中啟用。
# 有關停用特定 lint rules 和啟用其他 rules 的資訊,請參閱該檔案。
flutter_lints: ^1.0.0
# 有關此檔案的通用 Dart 部分的資訊,see the following page: https://dart.dev/tools/pub/pubspec
# 以下部分特定於 Flutter。
flutter:
# 以下行可確保將 Material Icons font 包含在你的應用程式中,以便你可以在 material Icons class 中使用 icons。
uses-material-design: true
# 要將 assets(一般是圖片) 新增到你的應用程式,請新增 assets 部分,如下所示:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# image asset 可以引用一個或多個特定於解析度的 "variants"(變體),see https://flutter.dev/assets-and-images/#resolution-aware.
# 有關從 package dependencies 中新增 assets 的詳細資訊,see https://flutter.dev/assets-and-images/#from-packages
# 要在應用程式中新增自定義字型,請在此處新增此 "flutter" 部分的字型部分。此列表中的每個條目應有一個帶有字型 family 的 "family" 鍵,以及一個帶有列表的 "fonts" 鍵,該鍵提供字型的 asset 和其他描述符。
# For example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# 有關 package dependencies 中字型的詳細資訊 see https://flutter.dev/custom-fonts/#from-packages
複製程式碼
(analysis_options.yaml 檔案後面再講,此部分先把引入 package 講完。)
在 pubspec.yaml
檔案中引入了 english_words: ^4.0.0
,除了可以引入依賴項之外,pubspec.yaml
還提供了許多其他的功能。
english_words
後面的 ^4.0.0
直接指明瞭這個 package 的版本。當沒有指定版本號時我們可以使用 flutter pub upgrade --major-versions
指令把依賴的 package 升級到最新版本,也可以手動修改版本號為一個指定版本,指定為當前最新版本來進行手動更新。要檢視哪些依賴的 package 有較新的版本可用時可執行:flutter pub outdated
指令檢視。我們當前是最新的,所以列印沒有發現。
hmc@bogon startup_namer % flutter pub outdated
Showing outdated packages.
[*] indicates versions that are not the latest available.
Found no outdated packages
hmc@bogon startup_namer %
複製程式碼
執行 flutter pub upgrade --major-versions
指令會有如下列印。看到 cupertino_icons
最新是 1.0.3
了,由於在 pubspec.yaml
檔案中我們直接指定了 ^1.0.2
所以它並沒有被直接更新。還有 flutter_lints
最新的是 1.0.4
,然後在 pubspec.yaml
檔案中指定的是 ^1.0.0
,也不會進行強制更新。
hmc@bogon startup_namer % flutter pub upgrade --major-versions
Resolving dependencies...
async 2.8.2
boolean_selector 2.1.0
characters 1.1.0
charcode 1.3.1
clock 1.1.0
collection 1.15.0
cupertino_icons 1.0.3
english_words 4.0.0
fake_async 1.2.0
flutter 0.0.0 from sdk flutter
flutter_lints 1.0.4
flutter_test 0.0.0 from sdk flutter
lints 1.0.1
matcher 0.12.11
meta 1.7.0
path 1.8.0
sky_engine 0.0.99 from sdk flutter
source_span 1.8.1
stack_trace 1.10.0
stream_channel 2.1.0
string_scanner 1.1.0
term_glyph 1.2.0
test_api 0.4.3
typed_data 1.3.0
vector_math 2.1.0
No dependencies changed.
No changes to pubspec.yaml!
hmc@bogon startup_namer %
複製程式碼
然後 pubspec.yaml
檔案中也指定了版本號和構建號,然後還有引入圖片資源、字型資源的內容。
二、獲取 package
執行 flutter packages get
/ flutter pub get
來拉取 pubspec.yaml 檔案中引入的 package,它們會被下載到 /Users/hmc/.pub-cache/hosted/pub.flutter-io.cn
路徑中。可以看到有如下輸出:
hmc@bogon startup_namer % flutter packages get
Running "flutter pub get" in startup_namer... 565ms
hmc@bogon startup_namer % flutter pub get
Running "flutter pub get" in startup_namer... 579ms
hmc@bogon startup_namer %
複製程式碼
三、在程式碼中引入 package
在 lib/main.dart
檔案的頂部我們可以引入要使用的 package,如下:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
複製程式碼
這裡還有一個點,上面我們匯入了 english_words/english_words.dart
當我們下面的程式碼不使用其中的內容時,它們會呈現為灰色的,它可以指示我們匯入的庫尚未使用(到目前為止)。
四、使用匯入的 package
下面我們使用 english_words
package 中的函式生成的英文單詞字串來替代 Hello World
。
Tip: 駝峰命名法(稱為 "upper camel case" 或 "Pascal case"), 表示字串中的每個單詞(包括第一個單詞)都以大寫字母開頭。所以,"uppercamelcase" 變成 "UpperCamelCase"。
程式碼修改如下:
import 'package:flutter/material.dart';
// ⬇️ 引入 english_words 中的內容
import 'package:english_words/english_words.dart';
void main() { runApp(new MyApp()); }
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ⬇️ 呼叫 WordPair 類的 random 函式,生成一個隨機單詞對
final wordPair = new WordPair.random();
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
// ⬇️ 把 "Hello World" 註釋了,然後 wordPair 轉換為駝峰命名法,顯示到螢幕中心
// child: new Text('Hello World'),
child: new Text(wordPair.asPascalCase),
),
),
);
}
}
複製程式碼
五、熱過載測試 package 的使用
如果應用程式正在執行,請點選熱過載按鈕 (⚡️閃電圖示:Hot Reload) 更新正在執行的應用程式。每次點選熱過載或儲存專案時,都會在正在執行的應用程式中隨機選擇不同的單詞對,可看到模擬器中心顯示不同的單詞對。 這是因為 wordPair
變數是在 build
方法內部生成的,每次 MaterialApp
需要渲染時或者在 Flutter Inspector 中切換平臺時 build
函式都會(重新)執行,此時 final wordPair = new WordPair.random();
便生成了新的 wordPair
變數。(我們可以把 final wordPair = new WordPair.random();
提出來,放到 build
函式的上面,再進行熱過載,可發現模擬器螢幕中心的單詞不再變化了。)
新增一個 有狀態的部件(Stateful widget)
在學習 Stateful widget
之前,我們再回顧一下上面的例項程式碼,其中最引我們矚目的應該是 class MyApp extends StatelessWidget {...}
,看到 MyApp
繼承自 StatelessWidget
,而我們這一小節的學習內容則主要與 StatefulWidget
類有關。
Stateless widgets 是不可變的,這意味著它們的屬性不能改變,所有的值都是最終的。Stateful widgets 持有的狀態可能在 widget 生命週期中發生變化。
實現一個 Stateful widget 至少需要兩個類:
- 一個
StatefulWidget
類。 - 一個
State
類。StatefulWidget
類本身是不變的,但是State
類在 widget 生命週期中始終存在。
在本小節中我們將新增一個繼承自 StatefulWidget
類的子類:RandomWords
,重寫 RandomWords
類的 createState
函式,返回一個 State
的子類 RandomWordsState
的例項物件。State
類將最終為 widget 來維護建議的和喜歡的單詞對。(一個 WordPair
陣列存放建議的單詞對,一個 WordPair
集合存放喜歡的單詞對。)
下面我們開始本小節的內容。
- 新增有狀態的
RandomWords widget
到 main.dart 檔案的底部。它可以在 MyApp 之外的檔案的任何位置使用,但是本示例將它放到了 main.dart 檔案的底部。在RandomWords widget
內部僅重寫了它的createState
函式,建立State
類,其他沒有做任何事情。下面我們分析一下createState
函式。
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
複製程式碼
State<StatefulWidget> createState()
函式用於在 widget 樹的給定位置建立可變 State
(函式返回值是 State<StatefulWidget>
)。StatefulWidget
子類應重寫此方法,並返回一個它們關聯的 State
子類的新建立的例項。
@override
State<MyWidget> createState() => _MyWidgetState();
複製程式碼
framework 可以在 StatefulWidget
的生命週期內多次呼叫此方法。例如,如果 widget
在多個位置插入到 widget
樹中,framework 將為每個位置建立一個單獨的 State
物件。類似的,如果 widget
從樹中移除並稍後再次插入到樹中,framework 將再次呼叫 createState
函式,以建立新的 State
物件,從而簡化 State
物件的生命週期。
- 新增
RandomWordsState
類。該應用程式的大部分程式碼都在該類中,該類持有RandomWords widget
的狀態。這個類將儲存隨著使用者滾動而無限增長的生成的隨機單詞對,以及使用者點選選中的單詞對,使用者通過重複點選心形 ❤️ 圖示來將單詞對從列表中新增或刪除。首先定義RandomWordsState
類,下面我們會一步一步為其新增內容。
class RandomWordsState extends State<RandomWords> {
// ...
}
複製程式碼
- 宣告瞭
RandomWordsState
類以後,IDE 會提示我們RandomWordsState
類缺少build
方法,我們重寫build
方法,並把之前在MyApp
中生成隨機單詞對的程式碼移動到RandomWordsState
中來生成單詞對。示例程式碼如下:
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
複製程式碼
- 如下示例程式碼,修改之前的舊程式碼,把生成隨機單詞對的程式碼從
MyApp
移動到RandomWordsState
中。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ⬇️ 把此行建立 wordPair 區域性變數註釋掉
// final wordPair = new WordPair.random();
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
// child: new Text('Hello World'),
// ⬇️ 註釋此行,使用下面的 RandomWords 例項物件,來返回一個隨機單詞對
// child: new Text(wordPair.asPascalCase),
child: new RandomWords(),
),
),
);
}
}
複製程式碼
重新啟動應用程式,應用程式還是會和之前一樣,每次熱過載或者 command + s 儲存程式,螢幕中心都會顯示一個新的單詞對。
下面這一段是上面示例的完整程式碼:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() { runApp(new MyApp()); }
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// final wordPair = new WordPair.random();
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
// child: new Text('Hello World'),
// child: new Text(wordPair.asPascalCase),
child: new RandomWords(),
),
),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
複製程式碼
建立一個無限滾動的 ListView
在上一節中,在 RandomWordsState
類的內部我們僅重寫了它的 build
函式,來返回一個隨機單詞對。而在這一節中,我們繼續擴充套件 RandomWordsState
類,以生成並顯示單詞對列表。當我們向上滑動時,ListView
將無限增長顯示隨機生成的單詞對。
ListView
的 builder
工廠建構函式允許我們按需建立一個懶載入的列表檢視(return new ListView.builder(...);
)。
- 首先我們向上一節建立的
RandomWordsState
類中新增一個final _suggestions = <WordPair>[];
陣列用以儲存建議的單詞對,該變數名用下劃線開頭,在 Dart 語言中使用下劃線做字首識別符號,會強制其變成私有的。另外新增一個_biggerFont
變數來增大字型大小。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
...
}
複製程式碼
- 向
RandomWordsState
類中新增一個_buildSuggestions()
函式,此方法用來構建顯示建議單詞對的ListView
。ListView
類提供了一個builder
屬性,itemBuilder
值是一個匿名回撥函式,它接受兩個引數BuildContext, int
,(context, i)
,i
是指行迭代器,迭代器從 0 開始,每呼叫一次該函式,i
就會自增 1,對於每個建議的單詞對都會執行一次,該模型允許建議的單詞對列表在使用者滾動時無限增長。
class RandomWordsState extends State<RandomWords> {
...
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 對於每個建議的單詞對都會呼叫一次 itemBuilder,然後將單詞對新增到 ListTile 行中,
// 在偶數行,該函式會為單詞對新增一個 ListTile row,
// 在奇數行,該函式會新增一個分割線 widget,來分隔相鄰的詞對。
// 注意,在小螢幕上,分割線看起來可能比較吃力。
//(這裡對比 iOS 中的 TableView,分割線是位於一個 Cell 上的,這裡的則是 分割線 和 每個單詞對都是一個 cell)
itemBuilder: (context, i) {
// 在每一列之前,新增一個 1 畫素高的分隔線 widget(從 0 開始,isOdd 判斷是否是奇數)
if (i.isOdd) return new Divider();
// 語法 `i ~/ 2` 表示 i 除以 2 的商(向下取整),返回值是整形(向下取整),比如 i 為:1,2,3,4,5 時結果則是:0,1,1,2,2,
// 由此可以計算出 ListView 中減去分隔線後的實際單詞對數量。
final index = i ~/ 2;
// 如果是建議單詞列表中最後一個單詞對,則進行擴容。
if (index >= _suggestions.length) {
// 接著再生成 10 個單詞對,然後新增到 _suggestions 中。
_suggestions.addAll(generateWordPairs().take(10));
}
// _buildRow 在下面進行解析。
return _buildRow(_suggestions[index]);
},
);
}
...
}
複製程式碼
- 對於每一個單詞對,
_buildSuggestions
函式都會呼叫一次_buildRow
函式。這個函式在ListTile
中顯示每個新的單詞對,這可以使我們在ListView
中生成每個顯示行,下面在RandomWordsState
類中新增_buildRow
函式。
class RandomWordsState extends State<RandomWords> {
...
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
// 每行顯示的文字,單詞對轉為駝峰命名形式
pair.asPascalCase,
// 字號是 18(`final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);`)
style: _biggerFont,
),
);
}
}
複製程式碼
- 下面我們更新
RandomWordsState
類的build
函式中的內容,讓其使用我們上面編寫的_buildSuggestions()
函式,不再直接僅僅生成一個單詞對。
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
// final wordPair = new WordPair.random();
// return new Text(wordPair.asPascalCase);
// 這裡返回 Sacffold 例項,把之前 MyApp 中的內容接管過來。
return new Scaffold(
// 導航條
appBar: new AppBar(
// 標題
title: new Text('Startup Name Generator'),
),
// body 這裡則是呼叫 _buildSuggestions() 函式來構建一個 ListView
body: _buildSuggestions(),
);
}
}
複製程式碼
- 更新
MyApp
類中build
函式的內容。從MyApp
中刪除Scaffold
和AppBar
例項。這些將由RandomWordsState
類來接管,這也使得在下一步中從一個螢幕導航到另一個螢幕時,可以更輕鬆的更改導航欄中的路由名稱(導航條標題)。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// final wordPair = new WordPair.random();
return new MaterialApp(
// title: 'Welcome to Flutter',
// home: new Scaffold(
// appBar: new AppBar(
// title: new Text('Welcome to Flutter'),
// ),
// body: new Center(
// // child: new Text('Hello World'),
// // child: new Text(wordPair.asPascalCase),
// child: new RandomWords(),
// ),
// ),
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
複製程式碼
重啟應用程式,我們將看到一個單詞對列表,儘可能的向下滾動,我們可以無限滑動,能一直看到新的單詞對生成。
新增互動
在這一步中,我們將為 ListView
的每一行新增一個可點選的心形 ❤️ 圖示。當我們點選 ListView
的條目時,會切換此條目的 “收藏” 狀態,將該單詞對新增或移除出 “收藏夾”。
- 新增一個
final _saved = new Set<WordPair>();
集合到RandomWordsState
類中,這個集合用來儲存使用者點選喜歡的單詞對。使用Set
比List
更合適,Set
可以保證元素的唯一性。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
...
}
複製程式碼
- 在
_buildRow()
方法中新增一個final alreadySaved = _saved.contains(pair);
來檢查確保單詞對還沒有被新增到收藏夾中。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}
複製程式碼
- 同時在
_buildRow()
中,新增一個心形 ❤️ 圖示到ListView
以啟用收藏功能。接下來就可以給心形 ❤️ 圖示新增互動能力了。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
// 新增心形圖示
trailing: new Icon(
// 並根據當前的單詞對是否已經被收藏,心形圖示顯示為不同的樣子(空心的表示未收藏,實心紅色表示已經收藏)
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
);
}
複製程式碼
-
重啟應用,可看到每一行單詞對的右邊都有一個空心的心形圖示,此時它們還沒有互動事件。
-
在
_buildRow
中讓心形圖示變的可以點選(注意這裡的互動事件是新增在 ListView 的每一行上面的)。如果單詞對已經新增到收藏中,再次點選將其從收藏夾中刪除。當每行單詞對被點選時,函式呼叫setState()
通知框架狀態已經改變。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
// 新增心形圖示
trailing: new Icon(
// 並根據當前的單詞對是否已經被收藏,心形圖示顯示為不同的樣子(空心的表示未收藏,實心紅色表示已經收藏)
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
// ListView 的每行新增互動事件
onTap: () {
setState(() {
// 點選事件,如果當前單詞對已經被收藏了則把其從 _saved 集合中移除,否則新增到 _saved 集合中
if (alreadySaved) {
// 移除
_saved.remove(pair);
} else {
// 新增
_saved.add(pair);
}
});
},
);
}
複製程式碼
Note: 在 Flutter 的響應式風格的框架中,呼叫
setState()
函式會為State
物件觸發build
方法,從而導致對 UI 的更新。
導航到新頁面
在這一節中,我們將新增一個顯示收藏夾內容的頁面(在 Flutter 中稱為路由(route)),並將學習如何在主路由和新路由之間導航(切換頁面)。
在 Flutter 中,導航器管理應用程式的路由棧,將路由推入(push)到導航器的棧中,將會顯示更新為該路由頁面。從導航器的棧中彈出(pop)路由,將顯示返回到前一個路由。
- 在
RandomWordsState
類的build
方法中為AppBar
新增一個列表圖示,當使用者點選列表圖示時,包含收藏夾的新路由頁面入棧顯示。
Note: 某些 widget 屬性需要單個 widget(child),而其它一些屬性,如果 action,需要一組 widgets(children),用方括號 [] 表示。
將該圖示及其相應的操作新增到 build
方法中:
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
// final wordPair = new WordPair.random();
// return new Text(wordPair.asPascalCase);
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
// actions 是一組互動,我們這裡僅需要一個跳轉到收藏夾路由的互動圖示
actions: <Widget>[
// 收藏夾按鈕,點選時呼叫 _pushSaved 函式,它的圖示樣式是 Icons.list
new IconButton(onPressed: _pushSaved, icon: new Icon(Icons.list)),
],
),
body: _buildSuggestions(),
);
}
...
}
複製程式碼
- 向
RandomWordsState
類中新增_pushSaved()
函式。
class RandomWordsState extends State<RandomWords> {
...
void _pushSaved() {
// ...
}
}
複製程式碼
熱過載應用,導航欄右側會顯示一個列表按鈕樣子的圖示,現在點選它還不會有任何反應,因為 _pushSaved()
函式還沒有新增內容。
- 當使用者點選導航欄中右側的列表圖示時,建立一個路由並將其推入到導航管理器的棧中,此操作會切換頁面以顯示新路由。新頁面的內容在
MaterialPageRoute
的builder
屬性中構建,builder
是一個匿名函式。新增Navigator.push
呼叫,這會使路由入棧(以後路由入棧均指推入到導航管理器的棧)。
void _pushSaved() {
Navigator.of(context).push(
);
}
複製程式碼
- 新增
MaterialPageRoute
及其builder
。現在,新增生成ListTile
行的程式碼。ListTile
的divideTiles()
方法在每個ListTile
之間新增 1 畫素的分割線。該divided
變數持有最終的列表項。
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
// 遍歷 _saved 中收集的每個單詞對
final tiles = _saved.map(
(pair) {
// 根據每個單詞對構建一個 ListTile 行
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
// 在每個 ListTile 之間新增 1 畫素的分割線
final divided = ListTile.divideTiles(
context: context,
// 上面構建的內容
tiles: tiles,
).toList();
},
),
);
}
複製程式碼
builder
返回一個Scaffold
,其中包含名為Saved Suggestions
的新路由的導航條。新路由的body
由包含ListTiles
行的ListView
組成,每行之間通過一個分隔線分隔。
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
// 遍歷 _saved 中收集的每個單詞對
final tiles = _saved.map(
(pair) {
// 根據每個單詞對構建一個 ListTile 行
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
// 在每個 ListTile 之間新增 1 畫素的分割線
final divided = ListTile.divideTiles(
context: context,
// 上面構建的內容
tiles: tiles,
).toList();
// 新路由頁面
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
// ListView
body: new ListView(children: divided),
);
},
),
);
}
複製程式碼
- 熱過載應用程式,收藏一些單詞對,並點選導航欄上的列表圖示,在新路由頁面中顯示收藏的內容。注意,導航器會在導航欄中新增一個 “返回” 按鈕,我們不必顯式實現
Navigator.pop
,點選返回按鈕時便能回到主頁路由。
總結
至此我們便學會了一個簡單的可滾動、可互動、可路由的 Flutter 應用程式了,同時我們對 Flutter 應該也有一個大致的瞭解了,相比原生而言它的開發效率可太高了,熱過載也太愛了,原生動輒該一行程式碼都要重新編譯執行實在太“拉胯”了!那麼本篇就到這裡吧, 後續我們開始深入學習 Flutter!⛽️⛽️ ???
參考連結
參考連結:?