Flutter 學習筆記(一):第一個 Flutter 應用

鱷魚不怕_牙醫不怕發表於2021-08-31

入門: 在 macOS 上搭建 Flutter 開發環境

一、獲取Flutter SDK

 去 flutter 官網下載其最新可用的安裝包:Flutter SDK releases,或者去 Flutter 的 Github 倉庫去下載:flutter/flutter

二、更新環境變數

 新增 flutter 相關工具到 path 中:

export PATH=`pwd`/flutter/bin:$PATH
複製程式碼

 此程式碼只能暫時針對當前命令列視窗設定 PATH 環境變數,要想永久將 Flutter 新增到 PATH 中需要更新環境變數,以便你可以執行 flutter 命令在任何終端會話中。

  1. 開啟(或建立).bash_profile,此檔案在不同的機器上可能檔案路徑不同,例如在我的電腦上在此路徑:/Users/hmc/.bash_profile。如果 .bash_profile 檔案不存在的話可以 cdUsers/xxx(你當前的使用者名稱) 路徑下,然後使用 vim .bash_profile 命令自行建立一個。
  2. .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
複製程式碼
  1. 開啟一個終端視窗執行 source /Users/hmc/.bash_profile 命令進行重新整理(.bash_profile 檔案的實際路徑大家以自己的機器為準)。
  2. 如果終端使用的是 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,等待應用程式啟動,如果一切正常,在應用程式建立啟動成功後,應該在裝置或模擬器上看到應用程式截圖如下:

截圖2021-08-24 下午9.53.42.png

二、熱過載

 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 至少需要兩個類:

  1. 一個 StatefulWidget 類。
  2. 一個 State 類。StatefulWidget 類本身是不變的,但是 State 類在 widget 生命週期中始終存在。

 在本小節中我們將新增一個繼承自 StatefulWidget 類的子類:RandomWords,重寫 RandomWords 類的 createState 函式,返回一個 State 的子類 RandomWordsState 的例項物件。State 類將最終為 widget 來維護建議的和喜歡的單詞對。(一個 WordPair 陣列存放建議的單詞對,一個 WordPair 集合存放喜歡的單詞對。)

 下面我們開始本小節的內容。

  1. 新增有狀態的 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 物件的生命週期。

  1. 新增 RandomWordsState 類。該應用程式的大部分程式碼都在該類中,該類持有 RandomWords widget 的狀態。這個類將儲存隨著使用者滾動而無限增長的生成的隨機單詞對,以及使用者點選選中的單詞對,使用者通過重複點選心形 ❤️ 圖示來將單詞對從列表中新增或刪除。首先定義 RandomWordsState 類,下面我們會一步一步為其新增內容。
class RandomWordsState extends State<RandomWords> {
    // ...
}
複製程式碼
  1. 宣告瞭 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);
  }
}
複製程式碼
  1. 如下示例程式碼,修改之前的舊程式碼,把生成隨機單詞對的程式碼從 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 將無限增長顯示隨機生成的單詞對。

ListViewbuilder 工廠建構函式允許我們按需建立一個懶載入的列表檢視(return new ListView.builder(...);)。

  1. 首先我們向上一節建立的 RandomWordsState 類中新增一個 final _suggestions = <WordPair>[]; 陣列用以儲存建議的單詞對,該變數名用下劃線開頭,在 Dart 語言中使用下劃線做字首識別符號,會強制其變成私有的。另外新增一個 _biggerFont 變數來增大字型大小。
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}
複製程式碼
  1. RandomWordsState 類中新增一個 _buildSuggestions() 函式,此方法用來構建顯示建議單詞對的 ListViewListView 類提供了一個 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]);
      },
    );
  }
  ...
}

複製程式碼
  1. 對於每一個單詞對,_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,
      ),
    );
  }
}
複製程式碼
  1. 下面我們更新 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(),
    );
  }
  
}
複製程式碼
  1. 更新 MyApp 類中 build 函式的內容。從 MyApp 中刪除 ScaffoldAppBar 例項。這些將由 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 的條目時,會切換此條目的 “收藏” 狀態,將該單詞對新增或移除出 “收藏夾”。

  1. 新增一個 final _saved = new Set<WordPair>(); 集合到 RandomWordsState 類中,這個集合用來儲存使用者點選喜歡的單詞對。使用 SetList 更合適,Set 可以保證元素的唯一性。
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _saved = new Set<WordPair>();

  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}
複製程式碼
  1. _buildRow() 方法中新增一個 final alreadySaved = _saved.contains(pair); 來檢查確保單詞對還沒有被新增到收藏夾中。
Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  ...
}
複製程式碼
  1. 同時在 _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,
    ),
  );
}
複製程式碼
  1. 重啟應用,可看到每一行單詞對的右邊都有一個空心的心形圖示,此時它們還沒有互動事件。

  2. _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)路由,將顯示返回到前一個路由。

  1. 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(),
    );
  }
  ...
}
複製程式碼
  1. RandomWordsState 類中新增 _pushSaved() 函式。
class RandomWordsState extends State<RandomWords> {
  ...
  void _pushSaved() {
    // ...
  }
}
複製程式碼

 熱過載應用,導航欄右側會顯示一個列表按鈕樣子的圖示,現在點選它還不會有任何反應,因為 _pushSaved() 函式還沒有新增內容。

  1. 當使用者點選導航欄中右側的列表圖示時,建立一個路由並將其推入到導航管理器的棧中,此操作會切換頁面以顯示新路由。新頁面的內容在 MaterialPageRoutebuilder 屬性中構建,builder 是一個匿名函式。新增 Navigator.push 呼叫,這會使路由入棧(以後路由入棧均指推入到導航管理器的棧)。
void _pushSaved() {
    Navigator.of(context).push(
    );
}
複製程式碼
  1. 新增 MaterialPageRoute 及其 builder。現在,新增生成 ListTile 行的程式碼。ListTiledivideTiles() 方法在每個 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();
      },
    ),
    
  );
}
複製程式碼
  1. 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),
        );
      },
    ),
    
  );
}
複製程式碼
  1. 熱過載應用程式,收藏一些單詞對,並點選導航欄上的列表圖示,在新路由頁面中顯示收藏的內容。注意,導航器會在導航欄中新增一個 “返回” 按鈕,我們不必顯式實現 Navigator.pop,點選返回按鈕時便能回到主頁路由。

總結

 至此我們便學會了一個簡單的可滾動、可互動、可路由的 Flutter 應用程式了,同時我們對 Flutter 應該也有一個大致的瞭解了,相比原生而言它的開發效率可太高了,熱過載也太愛了,原生動輒該一行程式碼都要重新編譯執行實在太“拉胯”了!那麼本篇就到這裡吧, 後續我們開始深入學習 Flutter!⛽️⛽️ ???

參考連結

參考連結:?

相關文章