跳到主要內容

[入門02]跟著Flutter範例學習,快速上手

H Hello Word!
應該是所有人剛接觸任何一個新技術時,第一篇就是教大家寫出這段話吧!
呼~ 好佳在,Flutter不是純程式語言,它是 UI 啊!!!
所以,本篇除了開頭寫出,再來就不會出現囉~

一、正式前言

本篇完全參考 Codelabs 上的 Building Beautiful UIs with Flutter
讀者英文程度還可以的,直接看原文會更深入了解,接下來是給比較不想要看太多英文字串的人來閱讀。

環境建置就不再多說明,請先把 flutter 設定好,再執行
$ flutter doctor
檢查所有功能都正常就代表OK了

二、開始進入主題

首先,建立一個 Flutter 專案。接著可以把原本的程式碼清乾淨囉

預期畫面最後會像這樣
iOSAndroid


1、標題先搞定

目標建立一個 MateriaApp 包一個 Scaffold 再定義 AppBar
main.dart
// 取代原先在 main.dart 中的程式碼

import 'package:flutter/material.dart';

void main() {
    runApp(new FriendlychatApp());
}

class FriendlychatApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return new MaterialApp(
            title: "Friendlychat",
            home: new ChatScreen(),
        );
    }
}

class ChatScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return new Scaffold(
            appBar: new AppBar(title: new Text("Friendlychat")),
        );
    }
}

簡單說明一下這個步驟要完成的事:
  • 所有看得到的元件都定義成 StatelessWidget
  • 每個StatelessWidget都複寫一個方法:build()
  • 而元件 Scaffold 及 AppBar 都是定義在 Material Design 中。而 Text 則是一般的元件,任何地方都可以使用。

接著按下 reload 按鈕後,畫面就會像這樣:
iOSAndroid

2、加入送出訊息的功能

預期在使用者按下文字輸入框時要跳出鍵盤,輸入完文字後可以按下送出鍵。送出鍵被按下,需要判斷是否空字串,空字串就不做任何動作;如果有文字就送出並清空文字輸入框。
然後,文字被送出後,在畫面中也要呈現使用者輸入的文字訊息。

加入互動式的輸入文字框

Flutter 提供了一個可個行定義字段行為屬性的動態元件 TextField。它可以同步讀取訊息,並可以在它的生命期週裡更改訊息內容。但要把它加入本專案時,還是需要做一些小修改。

在 Flutter中,如果想要直接呈現動態數據,需要先將訊息數據封裝在 State 中。然後,將 State 物件繼承於 StatefulWidget (注意,不是先前的 StatelessWidget)。

所以接下來,要把 ChatScreen 改繼承 StatefulWidget。而要讓 TextField 可以處理文字,再定義一個 ChatScreenState 並繼承 State,並且將 AppBar 改寫至此類別中。

main.dart
// 修改 ChatScreen 類別並改繼承於 StatefulWidget.

class ChatScreen extends StatefulWidget { //修改此
    @override
    State createState() => new ChatScreenState(); //新內容
}

// 加入此類別

class ChatScreenState extends State { //新內容
    @override
    Widget build(BuildContext context) {
        return new Scaffold( //移到此
            appBar: new AppBar(
                title: new Text("Friendlychat")
            ),
        );
    }
}


改寫完成後,開始在 ChatScreenStats 中加入元件吧!
文字輸入框控制器
final TextEditingController _textController = new TextEditingController();

文字送出後的處理
void _handleSubmitted(String text) {
    _textController.clear();
}

放入元件,並定義
Widget _buildTextComposer() {
    return new Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: new TextField(
            controller: _textController,
            onSubmitted: _handleSubmitted,
            decoration: new InputDecoration.collapsed(
                hintText: "Send a message"),
        ),
    );
}

Container 就是一個容器的概念,margin 定義左右邊距為8,但在 iOS 是 px,Android 則是 dp
child 為加入的內容物 TextField,並定義行為:
  • controller 為 _textController
  • onSubmitted 送出時的處理行為 _handleSubmitted
  • decoration 修飾,加上 hint 讓使用者了解可以做些什麼
記得在 Scaffold 中加上 body,才能讓元件都呈現出來
@override
Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
            title: new Text("Friendlychat"),
        ),
        body: _buildTextComposer(), //這行
    );
}

接著畫面就改成這樣囉
iOSAndroid

加入送出按鈕

加入前,要先加入 Row 元件,才能讓排列變成橫向的方式。但為了讓畫面好看,而且希望輸入框大一點,送出按鈕靠在最右側,就讓輸入框加到 Flexible 中,而按鈕需要放到另一個 Container 中
Widget _buildTextComposer() {
    return new Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: new Row(
            children: <Widget>[
                new Flexible(
                    child: new TextField(
                        controller: _textController,
                        onSubmitted: _handleSubmitted,
                        decoration: new InputDecoration.collapsed(
                            hintText: "Send a message"),
                    ),
                ),
                new Container( //new
                margin: new EdgeInsets.symmetric(horizontal: 4.0), //new
                child: new IconButton( //new
                    icon: new Icon(Icons.send), //new
                        onPressed: () => _handleSubmitted(_textController.text)), //new
                ), //new
            ],
        ),
    );
}


大至上是完成了,但是還是有一些地方覺得怪怪的。
原來是配色啊~
加上APP當前的主題配色之後,系色就會符合主題了。
Widget _buildTextComposer() {
    return new IconTheme( //new
        data: new IconThemeData(color: Theme.of(context).accentColor), //new
        child: new Container( //modified
            margin: const EdgeInsets.symmetric(horizontal: 8.0),
            child: new Row(
                children: <Widget>[
                    new Flexible(
                        child: new TextField(
                            controller: _textController,
                            onSubmitted: _handleSubmitted,
                            decoration: new InputDecoration.collapsed(
                                hintText: "Send a message"),
                        ),
                    ),
                    new Container(
                        margin: new EdgeInsets.symmetric(horizontal: 4.0),
                        child: new IconButton(
                            icon: new Icon(Icons.send),
                            onPressed: () => _handleSubmitted(_textController.text)),
                    ),
                ],
            ),
        ), //new
    );
}

現在畫面長得符合主題囉

iOSAndroid

3、增加訊息列表的UI

準備讓聊天訊息出來見人啦!預期畫面要長成這樣
如畫面所示,上方要有一個可以滾動的列表並長到最高,接著是前一步實作的訊息輸入列,但在列表及輸入列中間,要有一層小陰影來呈現。每一個則訊息由 Row 包住。最左邊是圓形頭像、中間為直列排放的發件人名稱及消息內容的 Text,而訊息要長到最寬。

實作訊息列表

在這步驟,在呈現可以滾動的訊息列表前,先完成每則訊息的元件排列。 定義一個 StatelessWidget 的 ChatMessage,用 Container 包 Row。左邊是 Container 包 CircleAvatar,右邊是 Column 包 Widget列表,上 Text 下 Container 包 Text。
會用到 Container,就像前面說的都是為了可以加邊距之類的參數。
// 增加這個類別到 main.dart

class ChatMessage extends StatelessWidget {
      ChatMessage({this.text});
      final String text;
      @override
      Widget build(BuildContext context) {
        return new Container(
            margin: const EdgeInsets.symmetric(vertical: 10.0),
            child: new Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                    new Container(
                        margin: const EdgeInsets.only(right: 16.0),
                        child: new CircleAvatar(child: new Text(_name[0])),
                    ),
                    new Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                            new Text(_name, style: Theme.of(context).textTheme.subhead),
                            new Container(
                                margin: const EdgeInsets.only(top: 5.0),
                                child: new Text(text),
                            ),
                        ],
                    ),
                ],
            ),
        );
    }
}

上方程式中的 _name 請自行替換成發起人名稱,或參考另一篇文章可以匯入 firebase 上的資料。
// 加入這行程式到 main.dart

const String _name = "Your Name";


要有獨特風格的 CircleAvatar 元件,就把 _name 變數的第一個字符傳遞給子層的 Text 元件,便可以讓這個圖示變成名稱的第一個字。而兩層排列都有 CrossAxisAlignment.start,橫式排列就代表靠左,直式排列就代表靠頂端。

而發起人名稱的字體要大於訊息內容,所以在這套用了 Theme.of(context).textTheme.subhead),您可以前往 Material Design樣式 參考其定義。在此篇文章後,會依 Android 及 iOS 的不同系統而重新設置。

實作聊天訊息列表

接下來就要實作取得聊天訊息放入 UI 中顯示。也希望此列表可以滾動,方便使用者查看著聊天記錄,並且依照時間排序,最新的訊息會在下方。

接著要在 ChatScreenState 類別中增加一個 List 變數 _messages,用來儲存每個聊天訊息。而每個項目都是一個 ChatMessage 實例,剛開始會初始化為空的 List。
class ChatScreenState extends State<chatscreen> {
    final List<ChatMessage> _messages = <ChatMessage>[];    // new
    final TextEditingController _textController = new TextEditingController();

再把先前的 _handleSubmitted() 方法完全取代如下
void _handleSubmitted(String text) {
    _textController.clear();
    ChatMessage message = new ChatMessage(
        text: text,
    );
    setState(() {
        _messages.insert(0, message);
    });
}


setState() 是用來修改 _messages,並通知 framework 這個元件有修改,需要重新整理 UI。

放入訊息列表

目前步驟已準備好聊天息列表了。開始著手放入 ListView,先把下方程式替換,接著會說明
Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(title: new Text("Friendlychat")),
        body: new Column(    //modified
            children: <Widget>[    //new
                new Flexible(    //new
                    child: new ListView.builder(    //new
                        padding: new EdgeInsets.all(8.0),    //new
                        reverse: true,    //new
                        itemBuilder: (_, int index) => _messages[index],    //new
                        itemCount: _messages.length,    //new
                    ),    //new
                ),    //new
                new Divider(height: 1.0),    //new
                new Container(    //new
                    decoration: new BoxDecoration(
                        color: Theme.of(context).cardColor),    //new
                    child: _buildTextComposer(),    //modified
                ),    //new
            ],    //new
        ),    //new
    );
}

使用一般的 ListView 會有一個問題,就是它不會自動偵測內部元件的數量,也就是不會自動增減。所以在這改用 ListView.builder。
解說一下使用的基本元件:
  • Column,故名思義就是跟 Row 不同,為排列垂直的元件
  • Flexible 放在ListView外層,就是要讓ListView自動長至最高,同時讓 TextField 保持固定的大小
  • Divider 用於上下兩者間的水平間隔
  • Container 通常用來定義背景圖、填充、設定邊距及很多設定用。再宣告 BoxDecoration 物件,來定義背景色,就能讓訊息列表及輸入列有的顏色有所區別
而 ListView.builder 的參數又分別代表:
  • padding 為訊息內容的內縮空白
  • reverse 會使得 ListView 排列翻轉成貼近底部
  • itemCount 指定列表中的訊息數量
  • itemBuilder 用於取得每個元件的 index 函數
再次重新載入畫面後,就會變成
iOSAndroid

然後試著打些文字再送出,就可以看到
iOSAndroid

3、讓 APP 動起來

您可以將動畫效果加到 APP 中,讓使用者體驗更加流暢和直覺。
當使用者發送新訊息時,會讓訊息有動畫效果,而不是簡單地在訊息列中顯示一則新訊息。

在 Flutter 中的動畫被封裝成 Animation,包含 type value 及 status (例如向前、向後、完成及消失)。您可以在元件中新增動畫或監聽動畫物件的變動。再根據動畫物件屬性的變動, framework 可以重新整理 UI 並建立新的元件。

定義 AnimationController

使用 AnimationController 類別來指定動畫該如何運行。並可以指定它的持續時間及播放方向(正向或反向)。

建立一個 AnimationController 時,必須傳遞一個 vsync 參數,以防止已經不存在螢幕的元件還給予動畫效果。需要在 ChatScreenState 類別繼承中加上一個 TickerProviderStateMixin。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { // 修改這行
    final List<ChatMessage> _messages = <ChatMessage>[];
    final TextEditingController _textController = new TextEditingController();


在 ChatMessage 類別修改如下
class ChatMessage extends StatelessWidget {
    ChatMessage({this.text, this.animationController});    // 修改這行
    final String text;
    final AnimationController animationController;    //新增這行


再來修改 _handleSubmitted(),實作一個 AnimationController 物件並定義到 ChatMessage 實例中。設定動畫運行時間為 700 毫秒(採用較長的秒數是為了測試效果,一般來說應該設定少一點,才不會影響操作效能),並指定動畫向前播放。
void _handleSubmitted(String text) {
    _textController.clear();
    ChatMessage message = new ChatMessage(
        text: text,
        animationController: new AnimationController(    //new
            duration: new Duration(milliseconds: 700),    //new
            vsync: this,    //new
        ),    //new
    );    //new
    setState(() {
        _messages.insert(0, message);
    });
message.animationController.forward();    //new
}

增加 SizeTransition 元件

修改 ChatMessage 的 build() 方法,用 SizeTransition 包裝之前定義的 Container,而且給定一個子層的寬度或高度。而 CurvedAnimation 跟 SizeTransition 類別一起產生緩和的動畫效果。緩和效果會導致訊息在動畫開始時快速滑入,快停止前放慢速度。
class ChatMessage extends StatelessWidget {
    ChatMessage({this.text, this.animationController});
    final String text;
    final AnimationController animationController;
    @override
    Widget build(BuildContext context) {
        return new SizeTransition( //new
        sizeFactor: new CurvedAnimation( //new
            parent: animationController, curve: Curves.easeOut), //new
        axisAlignment: 0.0, //new
        child: new Container( //modified
            margin: const EdgeInsets.symmetric(vertical: 10.0),
            child: new Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                    new Container(
                        margin: const EdgeInsets.only(right: 16.0),
                        child: new CircleAvatar(child: new Text(_name[0])),
                    ),
                    new Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                            new Text(_name, style: Theme.of(context).textTheme.subhead),
                        new Container(
                                margin: const EdgeInsets.only(top: 5.0),
                                child: new Text(text),
                    ),
                        ],
                    ),
                    ],
                ),
            ) //new
        );
    }
}

配置動畫

設定動畫控制器在不需要此資源時釋放,這是一個很好的做法。下方的程式碼演示在 ChatScreenState 類別中如何通過複寫 dispose() 方法來實作。
// 將下列的程式碼加到 ChatScreenState 類別中

@override
void dispose() { 
    for (ChatMessage message in _messages) 
        message.animationController.dispose(); 
    super.dispose(); 
}

這時重新啟動您的APP,並試著輸入一些訊息。在此不是用熱重新加載,因為要清除任何沒有動畫控制器的元件。

如果想要進一步試驗的動畫效果,下面提供一些方式:
  • 於 _handleSubmitted() 方法中修改 duration 的值來加快或減慢動畫效果。
  • 通過使用 Curves 類別中定義的常數來指定不同的動畫曲線。可以參考 Curves 的曲線來決定。
  • 試著把 SizeTransition 大小變化效果改成 FadeTransition 的淡出淡入效果,當然有一些參數還是要自行修改。

4、套用觸擊事件

在這一個步驟中,將為 APP 提供一些複雜的細節,例如只能在有效長度的文字下才能按下發送按鈕,又以 Android 或 iOS 的外觀模式套用按鈕。

發送按鈕的內文感知

在目前程式碼中,文字輸入框就算沒有文字,發送按鈕也能被點擊並發出訊息。

首先,定義一個布林變數 _isComposing,用來偵測輸入框是否有文字被輸入,有就為 true。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
    final List<ChatMessage> _messages = <ChatMessage>[];
    final TextEditingController _textController = new TextEditingController();
    bool _isComposing = false;    // 增加這行

要在文字有所變更時收到通知,需要將 onChanged 回應傳遞給 TextField 函數。並用 setState() 來改變 _isComposing 的值。
然後在發送按鈕被按下後的 onPressed 事件中,判斷 _isComposing 是否為 true,並送出。為 false 時回應 null。
Widget _buildTextComposer() {
    return new IconTheme(
        data: new IconThemeData(color: Theme.of(context).accentColor),
        child: new Container(
            margin: const EdgeInsets.symmetric(horizontal: 8.0),
            child: new Row(
                children: <Widget>[
                    new Flexible(
                        child: new TextField(
                            controller: _textController,
                            onChanged: (String text) { //new
                                setState(() { //new
                                    _isComposing = text.length > 0; //new
                                }); //new
                        }, //new
                        onSubmitted: _handleSubmitted,
                            decoration:
                                new InputDecoration.collapsed(hintText: "Send a message"),
                        ),
                    ),
                    new Container(
                    margin: new EdgeInsets.symmetric(horizontal: 4.0),
                        child: new IconButton(
                            icon: new Icon(Icons.send),
                            onPressed: _isComposing
                                ? () => _handleSubmitted(_textController.text) //modified
                                : null, //modified
                        ),
                    ),
                ],
            ),
        ),
    );
}

當文字內容被清除時,修改 _handleSubmitted 為 _isComposing 改成 false。
void _handleSubmitted(String text) {
    _textController.clear();
    setState(() {    //new
        _isComposing = false;    //new
    });    //new
    ChatMessage message = new ChatMessage(
        text: text,
        animationController: new AnimationController(
            duration: new Duration(milliseconds: 700),
            vsync: this,
        ),
    );
    setState(() {
        _messages.insert(0, message);
    });
    message.animationController.forward();
}

現在 _isComposing 可以控制發送按鈕的行為及外觀。
  • 當使用者在輸入框輸入文字時,_isComposing 變 true,顏色設置為 Theme.of(context).accentColor。而當使用者按下發送按鈕後,系統則呼叫 _handleSubmitted()。
  • 當使用者沒有輸入任何文字時,_isComposing 變 false,而此元件的 onPressed 屬性設為 null 來禁用發送按鈕。而按鈕的顏色更改為 Theme.of(context).disabledColor。

顯示更高的文字輸入框

當使用者的文字內容超出界面的寬度時,應該要有換行的行為來顯示更多的訊息內容。但現在會被截斷而且還會有錯誤訊息在畫面上。為了確保訊息的正確,簡單的方法就是填加 Expanded 元件。

在這一步驟中,將在訊息文字框外加一個 Expanded 元件來充許像 Column 這樣的元件在子層可以限制其寬度或高度(在Column中則是限制其寬度)。

在這裡,可以使用 IntelliJ 的一個方便的功能(Android Studio 就是),下面圖片將示範如何快速加入:

說明:
  • 1、將游標移至 new Column 上。
  • 2、點擊左側的燈泡圖示,然後從彈出的菜單中選擇 『Wrap with new widget』,就填加了一個通用 new widget 的程式碼。快捷鍵可以更快實作這個動作,就是 option + return (macOS) 或 alt + Enter (Linux/Windows)。
  • 3、將游標放在出現警告的 widget 文字上,再按下上步的組合鍵,可以直接選擇 Expanded 來完成。如果有人跟小編我一樣都不會出現,也可以圈選 widget 文字後自己輸入 Expanded 即可。

再次執行熱執行,應該可以看到錯誤訊息不見了,而且過長的文字也換行成功了。

為 Android 和 iOS 定製化

在這之前,送出按鈕都長得一樣,而且是 Android 通用的圖示。但在 iOS 通常可能是用『Send』的文字按鈕來表示。為了處理不同平台要有不同的樣式來呈現,這步驟就會針對 iOS 給予 CupertionButton,而 Android 則是 Material Design 的 IconButton。
呈現如下:
iOSAndroid

首先,為 iOS 定義一個新的 ThemeData 並命名為 kIOSTheme,並設定主配色為淺灰色、可動元件為橙色、亮度為明亮。為 Android 定義另一個並命名為 kDefaultTheme,設定主配色為紫色、可動元件為橙色。
// 加到 main.dart

final ThemeData kIOSTheme = new ThemeData(
    primarySwatch: Colors.orange,
    primaryColor: Colors.grey[100],
    primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = new ThemeData(
    primarySwatch: Colors.purple,
    accentColor: Colors.orangeAccent[400],
);

修改類別 FriendlychatApp,使用 MaterialApp 中的 theme 來變改主題。在這可以用 defaultTargetPlatform 屬性來判別所在系統為何。
// 這行加到 main.dart.
import 'package:flutter/foundation.dart'; //new

// 下面則是修改 FriendlychatApp 類別

class FriendlychatApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return new MaterialApp(
            title: "Friendlychat",
            theme: defaultTargetPlatform == TargetPlatform.iOS    //new
                ? kIOSTheme    //new
                : kDefaultTheme,    //new
            home: new ChatScreen(),
        );
    }
}


我們也可以將主題套用到 AppBar 元件。在 AppBar 中新增 elevation 屬性,並定義 iOS 為 0.0(無陰影),Android 為 4.0。當然這是兩個系統一般的使用者界面設定,不照這樣的設定也是可以(略過此步驟)。
// 修改 ChatScreenState 類別的 build() 方法

Widget build(BuildContext context) {
    return new Scaffold(
    appBar: new AppBar(
        title: new Text("Friendlychat"),    //modified
        elevation:
            Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, //new
    ),


接著修改發送按鈕的樣式,下面的程式碼僅顯示修改按鈕的部份
// 這行加到 main.dart
import 'package:flutter/cupertino.dart'; //new

// 修改 _buildTextComposer 方法中的按鈕部份

new Container(
    margin: new EdgeInsets.symmetric(horizontal: 4.0),
    child: Theme.of(context).platform == TargetPlatform.iOS ?    //modified
    new CupertinoButton(    //new
        child: new Text("Send"),    //new
        onPressed: _isComposing    //new
            ? () => _handleSubmitted(_textController.text)    //new
            : null,) :    //new
    new IconButton(    //modified
        icon: new Icon(Icons.send),
        onPressed: _isComposing ?
            () => _handleSubmitted(_textController.text) : null,
        )
),

到這一步為止,在 iOS 的視覺上,可能訊息列表與 AppBar 之間並沒有明顯示的區別。可以再加上一點點陰影的效果,那麼就在 ChatScreenState build() 的 body 再加上一層 Container 來包覆原本的 Column。(在此可以用 new widget 技巧來加,會比較容易哦)
// 修改 ChatScreenState build() 中的 body
// 原先的 body: new Column( 會改成
body: new Container(    //新增的外層
    child: new Column(
        ...
    ),
    decoration: Theme.of(context).platform == TargetPlatform.iOS    //new
        ? new BoxDecoration(    //new
            border: new Border(    //new
                top: new BorderSide(color: Colors.grey[200]),    //new
            ),    //new
        )    //new
        : null    //new
), // Container結尾

熱重載後就會看到 iOS 與 Android 有不一樣的顏色、陰影和按鈕圖示。

結束囉

做到這,就已經結束所有的說明及實作範例。如果遇到編碼出現問題,可能是某個步驟還是不太了解,建議下載原文原版程式碼來參考:
git clone https://github.com/flutter/friendlychat-steps.git
在 offline_steps 資料夾中,有每一個步驟的原始碼來參照。
再來,就是了解如何串接 Firebase 來實作。

留言

這個網誌中的熱門文章

[入門01]第一次寫 Flutter APP 就上手

一、前言 看到 Google I/O 釋出的影片,想必大家都想要快點了解 Dart 與 Flutter。 因為有了這兩個的結合,再也不用寫鳥鳥的HTML嵌入APP。用了一堆網頁語法,省不到時間又開發不出好的體驗的APP。   我認為,會寫程式的人都看得懂 安裝方法 ,所以就不再細論如何安裝的步驟。而且全程都用 Android Studio 當範例工具。 Visual Studio Code  也是很大推,我個人也是用VS來寫 Golang + Web Service + Docker 後台程式。 二、建立第一隻Flutter程式 記得在 IDE 先預裝好 Flutter 套件,不然開啟新專案時是無法選擇 Flutter 專案的。       建立 Flutter 專案   這裡要註意,因為 Flutter 的 package 限制規則,專案名程不能使用英文小寫及_(下底線)之外的文字符號。   到這一步,就已經有第一隻程式可以執行了。點擊手機畫面的『✚』就能計數。看到程式碼,是不是比原生的 Android 及 iOS 乾淨許多了。想到就讓人興奮起來啦(手握拳) 三、解說 程式主要的起頭程式碼在 /lib/main.dart。 定義如下: void main() => runApp(new MyApp()); // 定義主類別 class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {     return new MaterialApp(       title: 'Flutter Demo', // APP名稱改這裡是沒有用的 XD       theme: new ThemeData(         primarySwatch: Colors.blue, // Theme定義       ),       // title bar 名稱       home: new MyHomePage(title: 'Flutter Demo Home Page'),     );   } } 修改APP名稱 Android: &l