Flutter教學:由入門至開發出一個互動式App

站在巨人的肩膀上:Flutter 函式庫

Flutter 是 Dart 程式語言的函式庫,裡面有製作 UI (使用者介面) 的各種元件。利用 Flutter 開發視窗程式、手機 App 或 Web App,只要寫一次程式碼,即可透過編譯器讓程式跨平台在個人電腦、手機、網頁上執行。在 Flutter 裡,所有元件都是特別設計過的 widget (小部件) 類別,用程式的話來說就是「Flutter 裡的所有元件都繼承自 widget class」。製作 App 的按鈕、文字、影像、手勢輸入、以及排版,都是 widget 類別。藉由使用這個類別,讓我們可以用少許的程式碼,就開發出一個 App。

基本 widget 介紹:https://docs.flutter.dev/development/ui/widgets/basics
Android 設計風格元件介紹:https://gallery.flutter.dev/#/
排版 widget 介紹:https://docs.flutter.dev/development/ui/widgets/layout

圖一、基本 widget 介紹 (圖片截圖自Flutter 官網)
圖二、Android 設計風格元件介紹 (圖片截圖自Flutter 官網)
圖三、排版 widget 介紹 (圖片截圖自Flutter 官網)

認識 Flutter App 的架構方式

物件導向程式設計,就是把軟體設計想像成現實生活中的物體來設計。比如手機,手機是由外殼、螢幕、電池、主機板等等元件組合起來的。接著來看 App,Flutter 把 App 也當成是由一個個 widget 組合起來的,如下圖四的範例。範例中,最上層是一個使用 Android 設計風格的 App,稱為 MaterialApp,裡面有 title 與 home 兩個屬性。在 home 屬性裡,放了名為 Scaffold 的元件。Scaffold 又有 appBar 和 body 兩個屬性。appBar 裡面放了 AppBar,AppBar 的 title 裡面放文字元件 Text。body 裡面放了排版元件 Center,Center 的 child 裡面放文字元件 Text。

圖四、一個 Flutter App 架構範例

我們來看一下這個 App 的程式碼長得什麼樣子,以及它執行後產生的 App 長得什麼樣子。請看下面程式碼。

首先,我們要使用 Flutter 函式庫,而且是使用它當中 Android 設計風格的元件,所以要匯入函式庫:
import ‘package:flutter/material.dart’;

接著在程式的入口 main() 裡面,其實只有一行程式碼 runApp();。runApp() 是 Flutter 裡特別的函式,它輸入一個 widget,然後把所輸入的 widget 產生與繪圖出來。對照上圖四與下方的程式碼,我們輸入一個 MateriaApprunApp() 執行。

MateriaApp 裡面,有 titlehome 兩個屬性。home 屬性指定給一個 Scaffold 元件。

Scaffold 裡面,有 appBarbody 兩個屬性。appBar 屬性指定給一個 AppBar 元件,且該元件內的 title 屬性指定給一個文字元件 Textbody 屬性指定給一個排版元件 Center,排版元件內的 child 屬性指定給一個文字元件 Text

以上所有的元件都繼承了 widget 類別,所以都是 widget。在這個範例裡,我們建構了一個 App,它在上方顯示一些文字,然後在中央也顯示一些文字。請按下 Run 執行按鈕,看看這個 App 的模樣。

用 Flutter 函式庫撰寫 App 程式,就像是組合積木一樣。每個 widget 都有不同的屬性與功能,我們指派內容給它們的屬性,就像是將積木組合起來。在接下來學習 Flutter 的章節裡,我們將從兩個角度來認識更多製作 App 的知識。

其一是認識更多 widget 元件。在上面的程式範例裡,我們見到五個 widget,分別是 MaterialAppScaffoldAppBarCenterText。讀者目前可能只知其名而不知其內容,所以在接下來的章節將逐一介紹各個 widget 的功能與使用方式。在 Flutter 裡還有更多豐富的 widget 等著我們認識與使用。認識越多 widget,在建立自己的 App 時就有越多材料可以選擇,能讓畫面更精美、更多元的 UI 介面。

其二是認識更多 Flutter App 的控制方式,賦予 App 與使用者互動的功能。在上面的 App 範例中,只有顯示靜態的文字,完全與使用者沒有任何互動。如果希望 App 有些按鈕,使用者按下去之後會更新畫面、顯示一些互動的內容,或者製作遊戲 App,那麼就必須賦予 App 能回應使用者的操作行為。 再比如一個購物的 App,在購物的步驟中有許多操作頁面,我們希望有「下一步」、「上一步」、「回到首頁」的功能,這種在操作頁面之間跳來跳去的功能稱之為「Navigator (導航)」。認識越多功能,在建立自己的 App 時就有越多技術可以發揮,能提供使用者更多元的互動。

練習:

在上面的程式範例中,請嘗試更改 Text() 內的字串內容,然後重新執行。觀察一下你的更改,對應於 UI 顯示內容的變化。

認識 Andriod 風格的小部件 (material widgets)

Material 函式庫非常大,裡面有上百種元件讓大家使用,說明文件可參考:https://api.flutter.dev/flutter/material/material-library.html

在這個章節裡,將介紹最常用的幾個小部件帶大家入門。首先是文字元件 Text 和排版元件 Center。文字元件 Text,可以存放一些文字內容,設定文字的顯示方式如字型、字體大小,以及文字方向。排版元件 Center,可以將放在它的 child 屬性當中的元件,都排版放到中央。我們來看看下面的範例程式。

首先要使用 material 設計的 widget,就要匯入函式庫:import ‘package:flutter/material.dart’
在程式的入口 main() 裡面,其實只有一行 runApp(); 負責將 widget 繪製出來當成 App 來執行。
在 runApp() 裡面,放了一個 Center 元件,Center 的 child 屬性放了 Text 元件。
Text 元件的建構式,放入字串,以及指定 textDirection 屬性為 TextDirection.ltr。
ltr 意思是 left to right,讓文字靠左顯示。若設定為 TextDirection.rtl 就是靠右顯示。

練習:

在下方的程式範例中,請嘗試更改 textDirection 屬性。比較一下 TextDirection.ltr 和 TextDirection.rtl,對於 UI 顯示的差別。

各位同學可能有注意到,為什麼上面的程式碼範例,我是使用白色當底色,而不是用黑色當底色。因為,在沒有任何設定的前提下,App 的底色是無色的、文字是黑色的。如果我的程式介面使用黑色當底色,則按下 Run 執行程式碼後,各位將在右邊看到一片黑,黑色的文字無法在黑色的背景當中顯示。所以暫時使用白色當底色,讓同學可以看到黑色的文字。

這個簡短範例,示範了一個只會展示內容、不會與使用者互動的 App,就像是靜態網頁。所謂靜態網頁,就是內容固定不動的網頁,像海報一樣。我們需要設計海報的排版,分割成幾個區塊,為各區塊塗上底色、置放文字或圖片,最後將海報或靜態網頁呈現給大眾觀看。

假設我們要做一款 App,功能是顯示固定內容,就像我們去瀏覽一些景點介紹的網頁,網頁內容都固定不動。那麼,這些頁面就是靜態網頁。在 Flutter App 當中,稱呼這種固定內容的小部件為 StatelessWidget,沒有狀態的小部件。相對來說,如果小部件能儲存一些狀態數值,再根據使用者的互動來顯示內容,則動態內容的小部件稱為 StatefulWidget,有狀態的小部件。在更後面的章節會介紹 StatefulWidget。我們現在先學習基本的 StatelessWidget。

現在我們已經知道,撰寫 Flutter App 的步驟,就是先寫一個包含 App 全部內容的 widget,然後丟給 runApp() 去執行。要讓這個 widget 執行得有效率,Flutter 希望你告訴它這個 widget 是有狀態的還是無狀態的。用程式的話來說,就是讓你所寫的 widget 去繼承 StatefulWidget 或繼承 StatelessWidget。下面的範例,我將介紹更多常用的小部件,並讓我的 widget 繼承 StatelessWidget,也就是告訴電腦,我的 widget 是靜態的。我們先來看架構圖與排版。

圖五之一、一個 Stateless App 排版範例

看上圖的排版方式。由左到右為排版步驟。最左方是整個畫面。中間步驟將整個畫面分割成上下兩個區塊。上方區塊再透過右邊的步驟分割成三個水平區塊。

排版圖當中的英文單字都代表著小部件,下文在講解架構圖時,將詳細介紹各個小部件的功能與屬性。

圖五之二、一個 Stateless App 架構範例

看上圖右方的架構圖,我設計了一個 MyScaffold 類別 ,它繼承自 StatelessWidget,所以 MyScaffold 也是一個 widget,可以丟給 runApp() 去執行。
在 MyScaffold 裡面,我建立了一個 MaterialApp,代表我要讓底下的元件都使用 Android 設計風格。接著將 SafeArea 放入 MaterialApp 當中。
SafeArea 會偵測手機或網頁的顯示面板是否有遮蔽區域,有的話會避開,讓內容只顯示在不會受到遮蔽的區域。如下圖比較。

未使用 SafeArea (圖片連結自 Imgur)
使用 SafeArea (圖片連結自 Imgur)

在 SafeArea 內,我放入了排版元件 Column

排版元件 Column 的功能是將它所屬的區域 (在此例它的所屬區域為 SafeArea 的繪圖空間) 可以垂直擺放多個小部件,如右上圖所示。在 Column 內,我放入了兩個小部件,第一個是排版元件 Container,第二個是排版元件 Expanded

排版元件 Container 的功能是當一個容器,想像它就像一張畫布。可以決定這個畫布的大小、位置、邊框、底色、從哪裡開始畫圖,如右下圖所示。我將一個 Row 排版元件放到容器裡。在下方的程式碼當中,我設定了容器的高度 height 為 64 個像素點,沒有設定寬度,所以它預設是佔滿所屬區域的寬度。設定它的留白區域 padding 為水平對稱 8 個像素。設定它的底圖或底色 decoration 為藍色。

排版元件 Row 的功能是將它所屬的區域 (在此例它的所屬區域為 Container 的繪圖空間) 可以水平擺放多個小部件,如右上圖所示。在 Row 內,我放入了三個小部件,都是排版元件 Flexible

排版元件 Flexible 可以填充所屬 Row 或所屬 Column 的可用空間。在同一個 Row 或 Column 當中的 Flexible 元件,彼此可以透過屬性 flex 決定個別 Flexible 元件要佔用多大比例的空間。以下面程式碼範例來說,三個 flex 數值分別是 1, 3, 2,數值加總為 6,則三個 Flexible 元件佔用的空間為全部的 1/6、3/6、2/6。Flexible 元件還有 fit 屬性,fit 屬性的預設是 FlexFit.loose,意思是讓放在該 Flexible 元件底下的子元件可以保持自已的最大尺寸。若把 fit 屬性設定為 FlexFit.tight,則放在該 Flexible 元件底下的子元件會填滿該 Flexible 元件所佔有的繪圖空間。(註:關於 loose 與 tight 的差別,可以點這裡看 YouTube 影片)

最後介紹排版元件 Expanded,它其實就等於 fit 屬性設定為 FlexFit.tight 的 Flexible 元件,也就是讓它的內容填滿它所屬繪圖空間。

Row 與 Column 排版元件示意圖
Container 排版元件示意圖

請執行上方的程式看看。產生 App UI 之後,嘗試移動一下介於程式碼和 UI 畫面之間的垂直欄,更動 UI 畫面的寬度。你會發現,文字 Hello, world! 會自動調整位置,始終置放在畫面的中央。在上方的區域有藍色做底色,並且被分割成三個小區塊,三個區塊個別佔用 1/6, 3/6, 2/3 寬度。

接下來說明一下程式碼。程式碼最上方,匯入 material 函式庫,已經看了很多遍了,應該不需要再解釋。
程式碼接著是 main() 與 runApp() 我們也看很多遍了,MyScaffold 丟給 runApp() 執行。不需要再解釋。
程式碼的後面接著我們自己定義的 MyScaffold 類別。以下詳細介紹。

首先,它繼承自無狀態小部件,extends StatelessWidget,所以它也是 widget,可以給 runApp() 執行。
繼承了 StatelessWidget,有一些寫法要符合它的規範。第一個是建構式,照抄就對了。
我的類別名稱是 MyScaffold,那麼建構式就是 const MyScaffold({Key? key}) : super(key: key);
其次是要覆寫它預設的 build() 函式,覆寫方式讓 build() 回傳自己設計的 Widget。
請先照抄,再將藍色字的部分改為自己設計真實的 widget 程式碼。

@override
Widget build(BuildContext context) {
      return 自己設計的Widget;
}

我設計的 App 想採用 Android 手機的設計風格,所以讓 build() 函式 return MaterialApp();
MaterialApp 有許多屬性不一一介紹,在此只使用它的 home 屬性,代表這個 App 的 home 頁面。
我將它的 home 屬性設定給 SafeArea 小部件:

@override
Widget build(BuildContext context) {
      return MaterialApp( home: SafeArea() );
}

SafeArea 有許多屬性不一一介紹。在此只使用它的 child 屬性,代表它繪畫內容的區域。
我將它的 child 屬性設定給 Column 排版元件:

@override
Widget build(BuildContext context) {
      return MaterialApp( home: SafeArea( child: Column() ) );
}

Column 排版元件有許多屬性,必須要設定的是 children,該屬性接受小部件陣列型態。
也就是說,要把一堆 widget 放到陣列 [] 裡面:[widget_1, widget_2, …] 然後指派給 children
我將一個 Container 和一個 Expanded 放到陣列裡,指派給 children

@override
Widget build(BuildContext context) {
      return MaterialApp( home: SafeArea( child: Column( children: [Container(), Expanded()]) ) );
}

接下來的工作,就是將 Row 設定給 Container 的 child,然後繼續設定 Row 內要放的小部件。以及設定 Expanded 內要放的小部件。放完的成果,就是上面的範例程式碼。程式範例講解至此,我們看到一個靜態 App 的建立過程,其實就是指定 widget 並設定屬性。

練習:

在上方的程式範例中,請嘗試更改 Container 的 height 屬性,Flexible 的 flex 屬性,觀察當屬性數值改變時,UI 畫面的顯示差異。

運用鷹架小部件 Scaffold,快速建構 App 框架

在上一個章節裡,我們用手動方式,一步一步建立 App 的頁面排版與內容。在這個章節裡,要介紹「認識 Flutter App 的架構方式」這個章節的範例程式中,出現的小部件 ScaffoldScaffold 是 material 設計風格 App 的的鷹架,也就是採用 material 設計風格的基本長相。上方會有擺放橫幅的區域 appBar,然後中央是內容顯示區 body,下方有擺放導覽項目的區域 bottomNavigationBar,可能還有一個按鈕 floatingActionButton 浮在內容的上方。

回顧「認識 Flutter App 的架構方式」章節的範例程式,它示範在 Scaffold 中設定 appBarbody。下方的範例程式,示範 Scaffold 和  AppBar 更多設定。

在上面的程式範例裡,我設定了 ScaffoldappBarbodyfloatingActionButton 三個屬性。

appBar 屬性指定了一個 AppBar 元件,並設定了該 AppBar 元件的 leadingtitleactions 屬性。AppBar 各屬性的詳細介紹請參考說明文件。我們看一下右邊的圖示,展示各個屬性的名稱以及在 AppBar 排版當中的位置。

Flutter 提供的小部件太多,每個小部件又有許多屬性,因此不可能逐一介紹。在這個教學中,只能挑一些比較常用到的來說明。在前面的程式碼當中,我們看到 icon 屬性使用了 Icon 圖片,例如 Icons.search 和 Icons.add。其他內建的 Icon 圖片介紹於網站:https://fonts.google.com/icons

AppBar 的屬性與對應的排版位置 (圖片連結自 flutter.github.io)

tooltip 屬性可以設定提示文字,也就是當滑鼠移到該元件上方時,要顯示的提示文字。onPressed 屬性可以設定當該元件被點擊時,要執行什麼動作 (函式),以回應使用者的操作行為。靜態的 StatelessWidget 無法更改 UI 顯示內容,動態的 StatefulWidget 則可以透過 onPressed 屬性設定回應的函式,改變 UI 顯示的內容。我們將在教導動態 App 時,介紹 StatefulWidget 的詳細使用方式。

在範例中,我將 Scaffold 的 body 屬性指定給 Align 小部件。Align 小部件的 alignment 屬性需要指定 Alignment 小部件,用來設定內容要顯示在所屬區塊的哪個位置。在之前的範例中用過的 Center 小部件,其實等於:Align (alignemtn: Alignment.center),也就是將內容顯示在中央。在此介紹的 Align 小部件,可以更靈活地設定顯示位置,如左上角、右上角、左下角、右下角…….乃至於所屬區塊內的任意位置。

學習至此,我們知道如何運用 Flutter 撰寫靜態的 App。也認識了一些常用 widget 的使用方式,包含排版的 Row、Column、Center、Expanded、Flexible、Container,文字小部件 Text,圖示小部件 Icon。以及整個 App 設計風格的 MaterialApp,安全顯示區域 SafeArea,還有 UI 鷹架 Scaffold。網路上有一些中文的 Flutter 學習資源可以參考:

各種教學資源的內容大同小異,都是在介紹各種常用 widget 的屬性、用法。換句話說,Flutter 初學者和老手之間的差別,在於認識 widget 的多寡。老手熟悉許多 widget,信手拈來使用,就讓 App 內容多彩多姿。而新手由於見識的 widget 太少,想要什麼功能都只能自己打造,既費時又容易出錯。因此建議初學者,在看完前面所有章節,懂得撰寫靜態 App 之後,下一個學習目標是認識更多 widget 的功能與使用方式。第一個章節提供的幾個網站超連結,有空請記得去看一看,認識更多 widget,對開發 App 的功力絕對有幫助。

基本 widget 介紹:https://docs.flutter.dev/development/ui/widgets/basics
Android 設計風格元件介紹:https://gallery.flutter.dev/#/
排版 widget 介紹:https://docs.flutter.dev/development/ui/widgets/layout

練習:

在上方的程式範例中,請嘗試更改 Align 的 alignment 屬性,將它設定為 Alignment.bottomCenter。觀察 UI 顯示的變化。請點此超連結認識更多 Alignment 的預設常數,例如 bottomLeft、bottomRight、centerLeft、centerRight、topCenter…….。請嘗試更改 Align 的 alignment 屬性,觀察 UI 顯示的變化。

牛刀小試,靜態頁面程式框架

上面看了靜態 App 的說明與範例,現在我們自己動手寫寫看吧!下面提供了 App 程式碼的基本框架。首先匯入了 fluttermaterial 函式庫。然後在 main() 裡面執行 runApp()。接著放入整個 App 的設計風格MaterialApp,然後在它的 home 屬性放入 SafeArea 使頁面內容不會被手機硬體遮蔽。最後,將自己設計的 App 內容直接放在 SafeArea child 屬性底下。

自己設計的 App 暫時命名為 MyHomeWork。同學們可以自己修改類別名稱,將 MyHomeWork 改為你想要的名稱。好了,你可以開始撰寫與設計你的 App 了,將 build() 函式裡 return 後面的中文字「請在這裡加入程式碼」修改為你的程式碼吧!

練習:

請在前面的練習當中,自選一個練習,將 App 的內容部分複製到牛刀小試的 App 框架裡,並修改文字或其他參數。這個練習,是測驗同學是否理解如何用 flutter 撰寫 App,以及透過小部件的屬性來改變版面的外觀。

作業七:

請參考右方的作業七圖示。嘗試撰寫一個靜態的 App,畫面以固定比例分割為三行,各行裡再放置文字。排版架構可參考圖示,最上層是 Column,在裡面放三個 Flexible,然後個別再放入 Text。

三行的高度比例隨意。行內的排版方式隨意。例如,你可以先在 Flexible 內放入排版元件 Center 或 Align,然後再將 Text 放入排版元件內。

建議在 DartPad 撰寫程式碼,執行時才能調整視窗大小,觀察三行的高度是否保持比例。確保程式正確執行。黑色的文字無法在黑色的背景中顯示,所以,你可以嘗試使用 Scaffold 或使用 Container,增加背景顏色,讓你的黑色文字可被看見。或修改文字顏色。

作業七圖示

使用 Flutter 製作響應式網頁

早期的網頁排版與字體大小固定,導致使用者螢幕大小或解析度不同,網頁的排版就會走樣。2011 年發展出響應式網頁設計 (Responsive Web Design, RWD) 的概念,意思是由程式主動偵測螢幕顯示的畫面大小,自動更改排版與字體,使不同螢幕的使用者能獲得相同的使用體驗。在這個章節裡,要示範使用 FittedBox 小部件,讓內容可以隨畫面縮放,以及設定 Text,讓過長的字能夠自動換行。在說明下方的程式範例前,先執行看看吧!然後幫我調整瀏覽頁面的大小,觀察一下當畫面大小改變時 UI 更動的方式。以下開始解說程式碼。

首先要 import 函式庫,第 1 行程式碼我們見過許多次,應該沒問題。
再來是 main() 與 runApp(),這應該也沒問題。我們看一下 runApp() 裡面輸入了什麼。
最外層是 MaterialApp,讓裡面的設計風格都採用 Anrioid 風格。然後是 SafeArea,前面教過了。TryFitText 則是我們自己要寫的類別。
我們還沒有要與使用者互動,只是單純撰寫靜態的頁面,所以是無狀態的 StatelessWidget,要繼承它,寫法如下:

class TryFitText extends StatelessWidget {
      const TryFitText({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
            return 等一下要寫;
      }
}

上面一小段程式碼的寫法,我們在前一個章節裡應該已經看過了。建構式照抄就對了。
最重要的是要寫 build() 函式,改寫藍色字「等一下要寫」的部分,也就是我們的 UI。
先用一個 Column,將畫面在垂直方向分成四個區塊,所以有四個 Flexible 在 Column 內。flex 參數各是 1,1,3,3。
前兩個 Flexible 裡面,都放入 Row,將區塊在水平方向再分成更小的區塊。
在兩個 Row 內都放入三個 Flexible,flex 參數都是 1,也就是三個區塊的大小都一樣。兩行總共六個區塊。
然後再為這六個區塊放入 FittedBox,這個小部件的功能就是讓它 child 的內容可以依照它的 fit 屬性設定的方式做調整。
我在 FittedBox 的 child 內一律放 Text,然後 fit 則示範六種不同的設定:

BoxFit.fill:讓內容物完全填滿可用空間,可能讓內容物的形狀改變
BoxFit.contain:保持內容物的形狀,在可用空間內盡量放大
BoxFit.cover:保持內容物的形狀,允許內容物超出可用空間
BoxFit.fitWidth:保持內容物的形狀,依可用空間的寬度做縮放
BoxFit.fitHeight:保持內容物的形狀,依可用空間的高度做縮放
BoxFit.scaleDown:保持內容物的形狀,只在可用空間小於內容物時縮小內容物,不放大

以上是整個 UI 的最上面兩行要展示的內容。
現在來設定下面兩行,也就是 Column 當中,第三、第四個 Flexible 的內容。
第三個 Flexible 裡面,我先放入一個 Container
Containeralignment 屬性指定 Alignment 小部件,用來設定內容要顯示在所屬區塊的哪個位置。
padding 屬性要指定 EdgeInsets 小部件,決定邊界留白的方式。
color 屬性要指定背景的顏色,可以由 Colors 當中挑選。在此範例中我挑了 Colors.blueGrey 當背景。
最後在 child 裡放入一個字串內容很長的 Text為了讓這串字能自動換行,要設定 Text 的 softWrap 為 true

在第四個 Flexible 裡面,我放入一個 Image。它的功能是負責將影像顯示於 UI。
Image image 屬性指定影像來源。我使用網路的圖,所以指定一個 NetworkImage ,輸入網址。
Image 還有 fit 屬性可以指定顯示方式,預設是 BoxFit.contain。

練習:

在上方的程式範例中,請嘗試為 Image 小部件,加入 fit 屬性,指定不同的設定方式,如 BoxFit.fill…….。然後重新執行,觀察圖片的顯示方式有何變化。建議您將程式碼複製起來,另外開一個 DartPad 頁面,貼上程式碼。這樣可以更完整地觀察當螢幕顯示空間大小改變時,響應式頁面如何即時調整內容。

練習:

請參考右方的練習圖示。嘗試撰寫一個靜態的 App,畫面以固定比例分割為三行,中間行裡再分隔為兩列。讓各排版區塊的內容可隨畫面縮放調整大小,內容隨意放置文字或圖片。

排版架構可參考圖示,最上層是 Column,在裡面放三個 Flexible,然後在第二個 Flexible 內放入 Row,再於 Row 裡面放兩個 Flexible。各 Flexible 裡放入 FittedBox,再於其中放入文字或圖片。

三行的高度比例隨意,中間兩列的寬度比例隨意。行內的排版方式隨意。例如,你可以先在 Flexible 內放入排版元件 Center 或 Align,然後再將 Text 放入排版元件內。如果放圖片,就不需要 FittedBox,可以直接使用 Image 的 fit 屬性調整顯示方式。

建議在 DartPad 撰寫程式碼,執行時才能調整視窗大小,觀察排版的高度與寬度是否保持比例,觀察文字或圖片是否隨視窗尺寸調整大小。確保程式正確執行。黑色的文字無法在黑色的背景中顯示,所以,你可以嘗試使用 Scaffold 或使用 Container,增加背景顏色,讓你的黑色文字可被看見。或修改文字顏色。

練習圖示

補充:解決 DartPad 上無法載入 NetworkImage

當我們在 DartPad 上使用 NetworkImage 時,可能會遇到無法載入圖片的情形,錯誤訊息顯示 “Trying to load an image from another domain?”。內建的 NetworkImage 無法處理所有網路的狀況。這時候,可以改用網頁的 <img> 標籤,也就是在自己的 App 裡面放入網頁瀏覽器的 <img> 標籤,使 App 可以開啟來自任意網路上的圖片。

我做了一個 MyImage 小部件,裡面包裝了一個網頁的 <img> 標籤。只要在創建時,指定屬性 imageUrl 為某個圖片網址,它就可以載入並顯示。這個小部件需要 html 和 ui 兩個函式庫,要匯入之後才能使用 MyImage。

在下面的範例程式裡,參考了一個網路圖片。如果使用 DartPad 內建的 NetworkImage 來開啟,就會遇到上述的網路錯誤訊息,無法開啟圖片。而使用我做的 MyImage 小部件,就能開啟該網路圖片。

回應點擊手勢與文字輸入

大多數的 App 都具備某些回應使用者操作的功能,例如使用者點擊畫面、雙擊、垂直拖拉、水平拖拉、長按…等等。建立互動式 App 的第一步是偵測使用者的輸入手勢,有兩種方式可以使用。

第一種是在分割排版區塊後,將 GestureDetector 放入某個區塊內,然後再將區塊的內容放在 GestureDetector 的 child 屬性內。當使用者對區塊進行手勢操作時,例如點擊畫面,就會執行 GestureDetector onTap 屬性指定的函式。函式有兩種指定方式,一種是先撰寫一個無參數的函式,然後將函式名稱指定給 onTap 屬性,當函式的內容很大很長時,建議用這個方式,程式的排版才不會亂:

void functionA() { …. 很長的一段程式碼 … }
指派給屬性 onTap: functionA,

另一種方式是直接撰寫無參數的匿名函式,指定給 onTap 屬性,當函式的內容很短時可以用這個方式:
onTap: () { …. 一兩行短短的程式碼 … },

我們來看下面的範例,如何偵測使用者的點擊手勢。在這個範例裡,將畫面分為上下兩區塊。上面的區塊沒有偵測功能。下面的區塊,先放入 GestureDetector ,再將一個 Container 放在 child 裡。並且指定 onTap 的函式,使用上面的第二種方法,匿名函式。當使用者點擊下方的區塊時,就會 print 一行字。這行字不會在 UI 畫面顯示,因為我們尚未教到動態頁面的 StatefulWidget。請點開程式碼下方的 Console 區塊,程式 print 的內容將顯示在 Console 區塊內。

有時候我們不想要整個區塊都回應使用者的操作,只想讓一些按鈕或小圖示回應。在 flutter 裡有一些小部件已經包裝了偵測手勢的功能,如 IconButtonElevatedButton FloatingActionButton,都具有 onPressed 屬性可以指定函式回應使用者的點擊操作。我們直接來看下面的程式範例吧!

下面的範例,是稍微修改自上面的範例程式。在 Container 內,又用 Column 分成上下兩個區塊,然後在上方的區塊內置入一個 ElevatedButton,並且指定它的 onPressed 屬性到一個匿名函式。當使用者點擊按鈕時,就會 print 一行字。請點開程式碼下方的 Console 區塊,程式 print 的內容將顯示在 Console 區塊內。同學們會發現,當我們點擊在 ElevatedButton 上時,onPressed 指定的函式將有反應。當我們點擊在 Container 上時,onTap 指定的函式將有反應。

使用者操作 App,除了手勢,另一個重要的操作方式就是文字輸入。在 flutter 裡,可以使用 TextField 小部件,讓使用者在欄位當中以鍵盤輸入文字。關於 TextField 的排版,預設是佔滿所屬區塊的全部寬度,以及一行文字的高度。要調整 TextField 的寬度,請使用 SizedBox 容器並設定寬度屬性 width,將輸入欄位放在容器裡。至於 TextField 的高度,則受到兩個屬性的控制,其一是最大行數 maxLines,預設是一行。另一個是 style,藉由指定字體風格 TextStyle 的屬性字體大小 fontSize 與字體高度 height,決定一行字的高度。

TextField 放入排版中,即可讓使用者輸入文字。對於使用者的輸入,有三個回應方式需要知道:
第一是 onChanged 屬性,每當文字內容改變時,就會執行一次 onChanged 所指定的函式,函式型態為 void,參數為 String。
第二是 onSubmitted 屬性,當使用者按下 “Enter” 鍵時,就會執行一次 onSubmitted 所指定的函式,函式型態為 void,參數為 String。
第三是 controller 屬性,你需要指定一個 TextEditingController,然後用 TextEditingControllertext 屬性取得使用者輸入的文字。

上述 onChangedonSubmitted 所指定的函式,可以用匿名函式的方式指定,如 onSubmitted: (value) {},
也可以寫一個函式,如 void func(String s) {},然後再指定,如 onChanged: func,

下面的範例程式,將展示 TextField 的三種回應方式。這個程式與前面我們看到的範例程式最大的不同,在於我們定義的類別 TryTextField 內有宣告變數 myController,由於 myController 的值有可能變動,因此不能以 const 的方式產生 TryTextField。請看一下 runApp() 部分的程式碼,我將 const 關鍵字移除了。還有 TryTextField 的建構式,也將建構式的 const 宣告移除了。在 TryTextField 裡:

宣告了一個型態為 TextEditingController 的變數 myController。並將它宣告為 final,意思是這個變數不能被繼承。
定義了一個函式 void func(String value) { …… },準備指派給 TextField onChanged 屬性。

對於 TextField() 的屬性設定:
onChanged 屬性指定給函式 func。
onSubmitted 屬性指定給匿名函式 (value) { …… }
controller 屬性指定給變數 myController

另外還放了一個按鈕 ElevatedButton,在它 onPressed 屬性指定的函式內使用 myController.text,取得使用者輸入的內容。

程式執行時,在輸入的欄位打字,觀察 Console 的顯示內容。按下 Enter 鍵,觀察 Console 的顯示內容。按下 Button 按鈕,觀察 Console 的顯示內容。透過這個範例,認識 TextField 的使用方式,以回應使用者的輸入內容。

練習:

請寫一個 App,擁有:
一個 TextField 欄位,讓使用者可以輸入文字。
一個 GestureDetector,讓使用者點擊畫面時,在 Console 端顯示出使用者輸入的文字。

提示:請參考上面幾個程式範例。你需要在類別裡宣告 TextEditingController 型態的變數,並指定給 TextField 小部件的 controller 屬性。關於排版,你可以將全部畫面都先指定給 GestureDetector,然後再放入 Container,在將 TextField 放在 Container 內。記得設定 GesutreDetector 的 onTap 屬性,以回應使用者點擊畫面。

補充:調整按鈕的外觀

Material 設計風格的按鈕,除了 ElevatedButton 還有 TextButton OutlinedButton。這三種按鈕的基本外觀如右圖。如果想使用 Icon 圖來當按鈕,也有 IconButton 可以使用。這些按鈕可以透過在 child 屬性放置 Text() 小部件來顯示文字,以及用 styleFrom() 函式來設定 style 屬性來調整外觀和內部字體:
ElevatedButtonstyleElevatedButton.styleFrom() 來設定。
TextButtonstyleTextButton.styleFrom() 來設定。
 OutlinedButtonstyleOutlinedButton.styleFrom() 來設定。

三種常用按鈕

完整的 styleFrom() 可設定的參數,請點上面的超連結至官方網站瀏覽。以下介紹幾個常用的參數與範例。各參數的設定方式,請點各參數的超連結至官方網站瀏覽。

backgroundColor,按鈕裡塗滿的顏色
disabledForegroundColor,當按鈕處於不可使用時的文字顏色
disabledBackgroundColor,當按鈕處於不可使用時的塗滿顏色
shadowColor,按鈕的陰影顏色 (想像按鈕凸出平面,所以有影子,影子的顏色)
padding,按鈕內距離邊界的留白
side,按鈕邊框的寬度與色彩設定
shape,按鈕外型的形狀:斜邊矩形 BeveledRectangleBorder、圓形 CircleBorder、圓角矩形 RoundedRectangleBorder、體育場形 StadiumBorder
alignment,內容物的對齊方式

頁面導航功能

目前為止,我們的 App 頁面都只有一頁。如果我們想擁有多個頁面,並且在前一頁輸入的內容,可以傳遞到下一頁,或者在瀏覽的頁面可以回到前一頁,這時候我們就需要使用 Navigator 小部件。在這個入門教學裡,介紹最基本的導航方式,運用頁面堆疊 (stack) 來管理頁面。想像一下我們把一堆書疊起來後,永遠只能看到最上面一本書,如果要看到下面的書,就必須要先把上面的書移除。頁面堆疊的導航方式就是把即將瀏覽的頁面疊在舊的頁面上面,這個疊的動作稱為 push,如果要回去瀏覽舊的頁面,就將目前瀏覽的頁面拿掉,從堆疊中拿走最上頁面的動作稱為 pop。

我們來看看下方頁面導航堆疊圖示。一開始在頁面堆疊中有第一頁,畫面顯示第一頁。將第二頁 push 到堆疊中後,堆疊的最上面為第二頁,所以畫面顯示第二頁。再將第三頁 push 到堆疊裡,堆疊的最上面為第三頁,所以畫面顯示第三頁。然後,pop 掉最上面的頁面,堆疊的最上面變成第二頁,所以畫面顯示第二頁。再 pop 掉最上面的頁面,堆疊的最上面變成第一頁,所以畫面顯示第一頁。

圖示頁面導航堆疊

假設有兩個頁面 FirstPage 與 SecondPage,預設入口是 FirstPage,點擊按鈕後進入 SecondPage。將 SecondPage 置入頁面堆疊的程式寫法是:
Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage();} ));

MaterialApp 內建 Navigator 小部件,可以直接使用。
Navigator.push() 函式,第一個輸入參數 context 照抄就對了,第二個輸入參數是 MaterialPageRoute()。
要設定 MaterialPageRoutebuilder 屬性,指定建立下個頁面的函式。函式需要接收本頁面的 context,回傳下個頁面。指定方式有三種:
第一種是使用匿名函式,builder: (context) { return SecondPage(); },
第二種是使用只有一行的匿名函式寫法,builder: (context) => SecondPage(),
第三種是寫一個函式 Widget func(BuildContext context) { return SecondPage(); } 然後指定,builder: func,

下方的範例程式裡,有兩個頁面。第一個頁面 FirstPage 中央有一個按鈕,點擊按鈕後會進入第二個頁面 SecondPage。第二個頁面 SecondPage 會偵測使用者的點擊手勢,隨處點擊一下就回到上個頁面。另外,由於這兩個頁面的最外層框架都使用了 Scaffold,且設定了 appBar 屬性,因此當頁面進入到第二頁之後,在上方 AppBar 的左側出現了一個向左的箭頭圖示,點擊該箭頭圖示也會回到上一頁。這是 Scaffold 的內建功能。

練習:

請寫一個 App,擁有兩個頁面:
第一頁使用 GestureDetector 偵測點擊手勢,點擊後進入第二頁
第二頁使用 ElevatedButton,按下按鈕後回到第一頁

提示:請參考上面範例程式的寫法,練習使用 Navigator。
當你點擊畫面卻沒反應,只有文字部分有反應時,請看下方解說,教大家如何讓整個 body 都有能反應 GestureDetector。

解決許多同學在練習時遇到的問題

在寫練習的時候,許多同學都遇到一個狀況:將 Button 改為 GestureDetector 後,點擊畫面都沒有反應,只有文字區域點擊有反應。直觀的想法是:
body: GestureDetector (child: Expanded(child: Container(child: Text() )))
文字放在 Container 內,然後 Container 被 Expanded 佔滿整個 body。語法上是正確的,但是程式執行後,就會跳出 script error 錯誤訊息!

這個問題出現在 Container 的特性。當 Container 的 child 沒有東西時,Container 會盡可能地放大,也就是不用 Expanded 它也會佔滿整個 body。
當 Container 的 child 有東西時,Container 會盡可能地縮小,包裹住它的 child,以上面範例來說,就是變得跟 Text 一樣大。
問題來了!在上面的程式碼當中,Container 被 Expanded 放大,但又被 Text 縮小。兩者衝突,因此跳出 script error 錯誤訊息!

解決辦法是移除 Expanded。直接為 Container 設定大小,並且指定背景顏色。如果要讓 Container 填滿整個所屬區塊,就把它的寬與高都設定為無限,寫法如下:
body: GestureDetector (child: Container(child: Text(), width: double.infinity, height: double.infinity , color: Colors.white))
這樣就可以讓整個區塊都能反應 GestureDetector 了。

當我們想把第一頁輸入的資料傳遞給第二頁時,需要讓第二個頁面的建構式可以傳入參數,並將傳入的參數儲存到第二頁的屬性變數。於是,第二頁就能使用第一頁傳進來的資料了。

在先前的範例,我們都沒有更動過建構式,全部都是照抄。現在,我們需要修改建構式,讓物件被建構時能傳入資料,依傳入的內容調整物件。我們先複習一下,第二頁 SecondPage 的建構式原本的寫法:
const SecondPage({Key? key}) : super(key: key);
再複習一下,第一頁的 Navigator.push() 當中,MaterialPageRoute 的 builder 指定為:
(context) { return SecondPage(); }

現在,我們要為第二頁添加一個字串變數 inString,然後修改建構式:
final String inString;
const SecondPage({Key? key, required this.inString}) : super(key: key);
然後,假設第一頁要傳給第二頁的字串變數為 text,則 MaterialPageRoute 的 builder 要改寫為:
(context) { return SecondPage(inString: text); }

透過以上簡單的改寫,就能將第一頁的資料傳遞給第二頁。我們來看看下方的範例程式吧!在下面的程式裡,第一頁有一個按鈕和一個 TextField 輸入欄位,當使用者按下按鈕後,第一頁會將 TextField 裡輸入的內容傳遞給第二頁,第二頁將這些輸入的內容顯示在中央。

動態頁面 (StatefulWidget)

前面教的都是靜態頁面,由無狀態的小部件 (StatelessWidget) 所構成,無狀態小部件建構出來後就不會再重建與重繪。現在要教的是動態頁面,也就是包含有狀態小部件 (StatefulWidget) 的頁面,有狀態小部件建構出來後,只要執行它的 setState() 函式,它就會把自己建構的所有小部件全部都重建與重繪。

參考右方圖示,考慮程式執行效能,你應該盡量讓 StatefulWidget 放在比較靠近架構的末端,只有必須重繪的部分重繪就好。如果讓 StatefulWidget 放在架構的根部,則每次要求重繪時,所有小部件都將重新建立與繪製。

回憶一下,前面章節教導 GestureDetector 的 onTap 或各種按鈕的 onPressed,可以用指定的函式回應使用者的手勢動作,但只能改變資料內容,以及 print 在 Console 窗口,無法重繪小部件。有狀態的小部件 StatefulWidget 擁有 setState() 函式,只要在回應使用者動作的函式當中加入一行呼叫執行 setState() 函式,在它執行完後,就會將 StatefulWidget 所建立的小部件全部重繪。

接下來,要撰寫 StatefulWidget 了,我們先回憶 StatelessWidget 的寫法。最重要的排版等工作,就是要撰寫在 build() 函式裡面:
class MyStateless extends StatelessWidget {
      MyStateless({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
            return Text(‘MyStateless’);
      }
}

Stateful 小部件會重建自己建構的所有小部件
將 Stateful 小部件放在根部,則每次 setState() 都會重建全部的小部件

再來看 StatefulWidget 的寫法作對照:

class MyStateful extends StatefulWidget {
      MyStateful({Key? key}) : super(key: key);
      @override
      State<MyStateful> createState() { return _MyStateful(); }
      // 通常簡寫成 State<MyStateful> createState() => _MyStateful();
}

class _MyStateful extends State<MyStateful> {
      @override
      Widget build(BuildContext context) {
            return Text(‘MyStateful’);
      }
}

上面的程式碼就是寫 StatefulWidget 的大架構。有三個重點:
1. 原本寫 StatelessWidget 時,只需要定義一個 class。現在寫 StatefulWidget,要定義兩個 class。第二個 class,命名前有一個底線 _,也就是 _MyStateful,並且 extends 繼承 State<MyStateful>。
2. 在第一個class裡,只建構式,以及覆寫了 createState() 函式:State<MyStateful> createState() { return _MyStateful(); }
3. build() 函式放在 _MyStateful 的定義裡面。

以上架構寫出來,剩下的工作就是:
(1) 在 _MyStateful 裡宣告變數與更改變數的函式,以及
(2) 修改 _MyStateful
build() 函式,在某處呼叫 setState() 執行更改變數的函式。

最後要認識的是前面提到的 setState() 函式。setState() 函式需要輸入一個 void 函式給它執行,所以有兩種常用的寫法:
1. 如果函式很短,可以直接用匿名函式的方式丟進去當參數:setState( () { 很短的程式碼 } );
2. 如果函式很長,就另外定義函式 void func() { 很長的程式碼 },再丟進去當參數:setState( func );
我們就直接來看範例程式怎麼寫吧!執行程式,在畫面中有個文字按鈕,請多按幾下,看看左方文字的變化。

[註] 範例程式中的符號 =>,用於「函式定義內只有一行指令」的縮寫:
{ return something; } 等於 => something;
{ runApp(……); } 等於 => runApp(……);

上面的範例程式,首先看撰寫 StatefulWidget 的架構:
1. 有一個 MyApp 繼承 StatefulWidget,以及一個 _MyApp 繼承 State<MyApp>。
2. 在 MyApp 裡面,只有建構式,以及覆寫了 createState() 函式:State<MyApp> createState() => _MyApp();
3. build() 函式在 _MyApp 裡面。

然後,我在 _MyApp 裡面加入:
(1) 變數 int num = 0; 以及修改變數值的函式 void clickNumber() { num++; }
(2) 在 build() 當中加入一個按鈕,然後在按鈕的 onPressed 屬性指定的匿名函式 () { setState( clickNumber ); } 當中呼叫了 setState() 函式。
(3) 每次觸發 onPressed 都會更改變數 num 的值,Text 小部件負責將 num 的值顯示出來。

再示範一個簡單的例子,這次要讓按鈕每按一次,就隨機更變按鈕的背景顏色。在範例中,會用到亂數與陣列,以及設定按鈕的 style 屬性。

關於亂數,記得要 import ‘dart:math’; 函式庫才能使用 Random() 物件的 nextInt() 方法。這部分在 Dart 語法基礎教學我們應該熟悉了。
比較特別的是 Colors.primaries,同學們可以點一下左邊文字超連結去看它的詳細介紹。它的型態是 List<MaterialColor>,在 List 裡放了幾個常用的主要顏色。
範例中更變按鈕顏色的方式,就是每次都從 List 當中隨機挑一個顏色出來,指定給 Color 變數 myColor。
讓按鈕的 style 屬性依照 myColor 的顏色顯示按鈕的背景,即可做到每按一次按鈕,就隨機更變按鈕的背景顏色。

練習:

請參考上面範例兩個程式寫一個 App,畫面當中有一個 Text 和一個按鈕:
1. 每按一次按鈕。就隨機產生 0~9 的整數。
2. 將產生過的整數,顯示於 Text 當中。
例如,按第一次產生亂數 5,Text 上顯示 5。按第二次產生亂數 7,Text 上顯示 57。按第三次產生亂數 9,Text 上顯示 579。

提示:
假設 int a 儲存新產生的亂數,String s 儲存顯示的字串。
令 s = “$s$a”; 即可用簡短的程式碼將已顯示過的字串和新產生的亂數值接成新的顯示字串。

牛刀小試,動態頁面程式框架

仿照前面靜態頁面程式框架,這邊也給了一個動態頁面的程式框架。你不一定要使用這個框架程式碼,全部都可以自己重寫。使用這個框架程式碼,只是可以幫你減少開始寫動態頁面 App 的第一分鐘而已。

動態頁面的 App 暫時命名為 MyHomeWork2。同學們可以自己修改類別名稱,將 MyHomeWork2 改為你想要的名稱。好了,你可以開始撰寫與設計你的動態頁面 App 了,將 build() 函式裡 return 後面的中文字「請在這裡加入程式碼」修改為你的程式碼吧!

期末專題範例:選擇題庫複習機

這個範例 App 展示了單選題的題庫複習功能,目的是幫助使用者記憶題庫。在研究程式碼之前,先玩一下它吧!如果展示 UI 的寬度不夠,可以移動一下分隔線,讓 UI 完全顯示。這個 App 有兩個頁面,第一個頁面是靜態的,負責歡迎訊息,第二個頁面是動態的,負責顯示選擇題與使用者互動。每次進入到第二個頁面,所有題目都會重新洗牌顯示次序,且各題目的答案選項也會洗牌。每做完一題,點選題目或畫面空白處,就會跳往下一題。使用者可以利用這個 App 反覆訓練題庫,直到熟悉內容。對於教師來說,可以增加題庫內容或更多章節,提供方便學生複習的工具。

程式由上而下分為五個區塊來解釋。第一個是題庫字串 questionBank,第二個是題目類別 Question,第三個是測驗模擬器 ExamSimulator,第四個是 App 入口頁面 WelcomePage,第五個是測驗頁面 QuestionPage。

題庫字串 questionBank,用多行字串儲存了整個題庫,每一行就是一題。
每題的結構固定為:題號. (選項答案) 題目文字 選項 選項 選項 選項。
在增加或修改題庫時,要注意保持一行一題,且各題的結構要符合上述規則。
因為 Question 類別是根據上述規則來解析題目,如果結構改變,可能造成解析錯誤!

題目類別 Question,負責將題目字串解析成問題、選項、答案。
並且將選項與答案洗牌,避免題目的選項次序固定。
這個類別設計了三個建構式,第一個是 Question._(),目的是取消預設建構式,讓每個 Question 在產生時一定要有內容。
第二個是 Question.empty(),產生空題目,也就是當題庫做完時,要顯示的內容。
第三個是 Question.set(String question),解析輸入的一行題目字串。主要都是呼叫這個建構式。

測驗模擬器 ExamSimulator,負責整個測驗的運作邏輯。
每次測驗都先執行 prepareExam() 準備考題,這個函式將題庫拆成 Question 陣列,並且將題目次序洗牌。
其餘成員方法負責控制目前要顯示給使用者的題目。
題目還沒做完就給予 Quesiton 陣列內的題目,如果都做完了就給予空題目 emptyQuestion,顯示題目做完的訊息。

入口頁面 WelcomePage,執行 App 後第一個顯示給使用者的頁面。是一個靜態頁面 (Stateless)。
頁面的排版設計簡單,最外層是骨架 Scaffold,在裡面放了一個 Column。
Column 裡面,依次放了 SizedBox、Text、Divider、SizedBox、ElevatedButton。
Text 負責顯示歡迎訊息,BlevatedButton 負責按下後,將使用者導航到測驗頁面。

測驗頁面 QuestionPage,負責將題目一題一題顯示給使用者,並且根據答題計算正確題數。是一個動態頁面 (Stateful)。
初始化頁面時會執行一次 inintState() 函式,在此建立測驗模擬器 examSimulator,並初始化整個測驗題庫和測驗狀態。
按下按鈕時執行 handleOptionButtonOnPress() 函式,對答案,顯示答對答錯的按鈕,計算答對題數。
按下螢幕畫面時執行 handleScreenOnTap() 函式,讓測驗模擬器進入下一題,取得目前應當顯示的題目。
頁面的排版也很簡單,上方為顯示題目的區塊,下方為顯示按鈕選項的區塊。為了讓程式碼容易讀,所以兩個區塊個別都以函式來建立。
buildQuestion() 函式建立題目區塊,裡面的 Text 負責將目前題目的內文顯示出來。
buildOptions() 函式建立選項區塊,由於有四個選項,所以再寫了一個 buildOption() 函式來建立單一選項按鈕。
內部函式 buildOption() 負責建立單一選項按鈕,注意它的傳入參數,讓每個按鈕代表的選項內容與答案都不同,只有外觀相同。

這個專題範例程式算是有點閱讀量的。整體約 300 行,去除空行與註解,大約 250 行程式碼。同學們要多花點時間來理解這個範例程式,裡面包含了這學期所有學到的技術,有 Dart 語法基礎,並使用 Flutter 函式庫設計 App。

在 Dart 語法基礎裡,我們學了變數 int、float、bool、String,陣列 List,匯入函式庫 math 與使用 Random 的亂數函式、自訂函式、if else 條件判斷、for while 迴圈、class 類別宣告。在 App 設計教學裡,我們學了 Flutter 函式庫的設計框架、MaterialApp、SafeArea、Scaffold,排版元件 Row、Column、Flexible、Container、SizedBox、Padding、Align、Expand,顯示文字的 Text 與顯示圖片的 Image,還有可以回應手勢的 GestureDetector 與各種 Button 元件。最後還學到頁面導航 Navigator 和動態頁面 StatefulWedget 的建立方式。

如果你能讀得懂這個範例程式,恭喜你可以驕傲地說自己懂得一點程式設計了!

將 Web App 作品掛上網

Dart 程式語言使用 Flutter 函式庫製作的 App 可以跨 Windows、Android、iOS、以及網頁等平台,同樣的一份程式碼,經過編譯之後,就能產生在不同平台運行的 App,包含 Web App。Web App 顧名思義,就是在網頁上執行的 App,只要使用網頁瀏覽器,就能像使用手機 App 一樣地使用 Web App。

前段文字牽涉到三個技術。第一,安裝編譯器。第二,設定編譯參數。第三,將編譯出的成品安裝到平台系統上。這堂課作為初學者的入門教學,不打算教導關於編譯方面的技術,因為安裝編譯軟體需要較高階的硬體設備,例如 16GB 的記憶體。安裝開發環境與編譯等技術,較適合放在進階課程,提供給想深入認識程式設計的同學。

作為替代方案,這學期我們已經使用 DartPad 當開發環境,不需要安裝任何軟體。同樣地,我們也將藉由 DartPad 讓你寫的 Flutter 程式碼不用透過編譯軟體就能在網頁上呈現 App。將作品掛上網,請照以下步驟執行。

  1. 請申請一個 GitHub 帳號,網址是:https://github.com/。GitHub 是線上代管程式碼的服務平台,我們將會把程式碼傳到 GitHub 上,並且在 GitHub 平台上建立網頁。
  2. 擁有 GitHub 帳號後,請登入。然後在頁面右上角的 + 圖示點一下,會彈出功能列,如右圖步驟二所示。請點選 New gist 這一項。點選後,你將進入 gist 的服務頁面:https://gist.github.com/你的github帳號名稱。gist 是 GitHub 的一個子服務,目的是讓人可以分享片段的程式碼。
  3. 在 gist 新增你的程式碼,加入方式如右圖步驟三所示。請照圖示操作,檔名務必填寫為 main.dart,將你在 DartPad 開發的程式碼貼到編輯區,然後按下綠色的 Create secret gist 按鈕,建立程式碼的分享網址。
  4. 建立私人 gist 後,可以瀏覽方才新增的程式碼。請觀看此時的網址位置,應該是這樣的:
    https://gist.github.com/你的github帳號名稱/你的程式碼亂數流水號
    以後只要分享這個網址,別人就能看到你的程式碼。請複製網址位置當中你的程式碼亂數流水號,在接下來的步驟將使用到。如果想修改程式碼,可以按下畫面中的 Edit 按鈕,即可進入編輯畫面。如右圖步驟四所示。
步驟二:登入 GitHub 後,進入 gists 服務
步驟三:在 gists 新增程式碼
步驟四:記下程式碼的流水號
  1. 接下來,只要將你在 gist 的程式碼指派給 DartPad,即可展示你的作品。請在 DartPad 的網址後面,加上一些參數,然後輸入你的程式碼亂數流水號。如下:
    https://dartpad.dev/embed-flutter_showcase.html?run=true&id=你的程式碼亂數流水號
    只要瀏覽這個網址,就能看到你的期末專題從程式碼變成一個 Web App 了。

期末專題範例程式馬放在這個網址:https://gist.github.com/YouChingLee/e782ed2890cb2801d006e1a1b1fa37cc
範例程式掛到網頁上,在這個網址:https://dartpad.dev/embed-flutter_showcase.html?run=true&id=e782ed2890cb2801d006e1a1b1fa37cc

期末專題

期末專題請開發一個 Web App,下面兩種方式擇一完成:

  1. 以專題範例 — 選擇題庫複習機 — 為基礎,(1) 修改題庫內的題目,你的題目要有 20 題以上。(2) 並修改程式碼,例如改變排版、內文、底色、字體大小、按鈕顏色、按鈕形狀、增加按鈕、增加其他功能……。請讓你製作的題庫複習 App 具備吸引人使用的完整性。
  2. 或者自己設計一個 App,僅要求 (1) 一定要使用動態頁面 (StatefulWedget) (2) 一定要有回應手勢動作 (Button 或 GestureDetector 皆可)。App 的主題與內容不拘,例如一個小遊戲,或一個功能計算機。請讓你製作的 App 具備吸引人使用的完整性。

為了完成專題,你需要去 GitHub 註冊一個帳號,然後將你的程式碼上傳到 gist,並且將你的 gist 位置流水號嵌入到 dartpad 的網址參數中。讓使用者只要獲得你的作品網址,就能使用你開發的 Web App。繳交你的專題作品,僅須給老師兩個網址:

  1. 程式碼上傳到 gist 的網址。例如:https://gist.github.com/你的github帳號名稱/你的程式碼亂數流水號
  2. 展示作品的網址。例如:https://dartpad.dev/embed-flutter_showcase.html?run=true&id=你的程式碼亂數流水號

老師收到這兩個網址,可以去看你的程式碼,以及看你的專題作品。製作期末專題時,有些習慣能幫助你減少錯誤、提升開發效率:

  • 建議使用 Chrome 瀏覽器製作專題,避免其他瀏覽器不支援 DartPad,發生無謂的困擾。
  • 撰寫程式時,請頻繁確認是否有錯誤訊息,或每寫幾行就執行結果是否如預期。不要等寫了一堆程式碼之後發現執行異常才檢查哪裡寫錯,會很難找出問題。
  • 建議寫程式時,先不要加 const 標籤。等到都寫完了,最後才加上 const。因為 const 標籤加或不加,只會影響執行效能,不影響功能。但在寫程式的過程中,如果某些地方加了 const,可能會影響後面的程式邏輯,造成編譯錯誤,整個程式無法執行。
  • 撰寫 App 程式的步驟,建議先複製貼上骨架程式碼 (例如牛刀小試章節的程式碼),然後依設計版面增加 Column,再為各 Column 增加 Row,然後逐步增加文字或圖片內容,最後加入回應手勢動作的功能。詳細建議步驟請參考官方教學網頁:https://docs.flutter.dev/development/ui/layout/tutorial