1. 程式人生 > >Flutter 窺探(三)

Flutter 窺探(三)

 

上篇博文已經建立了一個App 

這一章主要是建立一個Flutter App。如果你熟悉面向物件程式設計,有基本的程式設計概念(變數,迴圈,條件判斷等),那麼你不必要具備原有的Dart和移動開發經驗,,就可以輕鬆地理解完成這章內容。 

你將會學到:

  • Flutter app基本結構
  • 查詢並使用包來擴充套件功能
  • 使用熱載入加快開發週期
  • 如何實現一個有狀態元件(Stateful widget)
  • 如何建立一個無限,懶載入列表
  • 如何建立並路由到第二個螢幕
  • 如何使用app主題修改外觀

現在我們來討論如何更改和應用他

替換模板的lib/main.dart
移除原有工程中的模板程式碼lib/main.dart。輸入如下程式碼,可以檢視到UI中間顯示的“Hello World”。

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: "Weleocme to Flutter",
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Welecomt to Flutter"),
        ),
        body: new Center(
          child: new Text("Hello World"),
        ),
      ),
    );
  }
  
}

結果

  • 這個例子建立了一個Material app。Material是在移動端和web端上的設計標準。Flutter提供了豐富的Material元件。
  • main()方法聲明瞭胖箭頭(=>)符號表示法,這種寫法表示了man()方法是一個單行函式,即函式體只有一行程式碼構成。
  • App繼承StatelessWidget,這也使得App本身成為了一個元件。在Flutter中,幾乎所有物件都被認為是一個元件,包含對齊方式,內邊距和佈局。
  • Material元件庫中的Scaffold提供了App預設需要的appbar,title,和body屬性,body屬性包含了home screen的元件樹結構。元件的子元件結構可以相當複雜。
  • 元件的主要工作就是提供build()方法,描述如何展示自己及其他元件。
  • 這個例子中元件結構有一個Center元件中包含一個Text子元件組成。Center元件將其內的元件結構置於螢幕中央。


Step 2:使用外部包

這一步中,你將使用開源包english_words。這個包中包含了幾千個最常用的英文單詞和一些實用方法。

你可以在pub.dartlang.org找到english_words包,同時還有其他的開源包。

檔案pubspec.yaml管理者Flutter App的資源。在pubspec.yaml中,新增english_words(3.1.0或者更高)到依賴列表。如下程式碼中:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
#begin
  english_words: ^3.1.0
#over

介於begin 和over 之間的就是新加的依賴

在Android Studio編輯器中檢視pubspec.yaml檔案時,可以看到編輯器右上方有命令操作欄,點選Package get。這就是獲取english_words包操作。你會在控制檯命令列看到:

H:\dev\flutter\bin\flutter.bat --no-color packages get
Running "flutter packages get" in flutter_app...                 0.4s
Process finished with exit code 0

相當於以前的sync  同步下來程式碼了

在lib/main.dart中,新增對english_words包的匯入,如下顯示的匯入語句:

import 'package:flutter/material.dart';  
import 'package:english_words/english_words.dart';

在你輸入後,AS會因為你寫的匯入語句給出建議。表現在你輸入的匯入語句會變成灰色,這就是提示你匯入的庫還沒有使用過。

  1. 使用english_words包來生成文字,而不再顯示“Hello World”
    Tips:“Pascal case”(大駝峰規則)意思是在一個字串中的每個單詞,包括第一個單詞,都是以大些字母開頭。因此,“uppercamelcase”就是"UpperCamelCase"。

針對原有程式碼做出修改

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: "Weleocme to Flutter",
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Welecomt to Flutter"),
        ),
        body: new Center(
          child: new Text(wordPair.asPascalCase),
        ),
      ),
    );
  }
  
}

如果App正在執行,使用熱載入按鈕

更新app。每次點選熱載入按鈕,或者進行儲存時,你應該都能在執行的app上看到隨機選取的不同的單詞對。這是因為單詞對是在build()方法中產生,build()方法每次在MaterialApp需要渲染或者在Flutter Inspector中開啟Platform時被執行。

每次點選這個熱更新按鈕文字都發生變化 就說明依賴完成了


Step 3:新增有狀態元件

無狀態元件(Stateless widget)是不可變的,意味著他們的屬性無法改變——所有值是final的。

有狀態元件(Stateful widget)維持一個狀態值,此狀態值會根據元件生命週期而有所改變。實現一個有Stateful widget需要至少兩個類:

  1. StatefulWidget類用以建立例項;
  2. State類。

StatefulWidget元件本身是不可變的,但是State類在整個元件宣告週期過程中是始終存在的。

在這步中,你將會新增一個stateful widget,RandomWords以及建立對應的狀態State類,RandomWordsState。State類實際上為元件保留著喜歡的單詞對。

在main.dart檔案中新增RandomWords元件。這個類可以定義在檔案內任意位置,但是我將其定義在檔案末尾。

class RandomWords extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return new RandomWordsState ();
  }

}
  1. 新增RandomWordsState類。大部分的app功能程式碼都會在這個類中。這個類同時儲存使用者滾動列表過程中產生的所有單詞對,以及使用者新增喜歡或者移除的單詞對。
    下面新增對基本的了定義,來保證類檔案編譯通過
  2. 新增State類之後,IDE會提示缺少一個build()方法。下一步,需要將產生單詞對的程式碼移至這個build()方法中。

 

class RandomWordsState extends State<RandomWords>{
  @override
  Widget build(BuildContext context) {
    final wordPair=new WordPair.random();
    return new Text(wordPair.asPascalCase);
  }
}
  1.  
  2. 在RandomWordsState的build()方法中新增程式碼,整個檔案看起來像這樣
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) {

    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          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);
  }
}

重啟App執行。
目前修改的程式碼在執行起來之後,效果與之前的一樣,只是將無狀態元件換成了有狀態元件

 

Step 4:建立無限滾動列表

在這步中,你將擴充套件RandomWordsState類來展示一個列表。列表隨著使用者的滾動無限增粘。ListView的builder工廠方法可以按需要進行懶載入。

  1. 在RandomWordsState中新增 _suggestions 列表變數儲存生成的候選單詞對。變數以 (_) 開頭 ——在Dart中,下劃線開頭的變數強調私有

同時新增比變數 biggerFont 改變字型大小。

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}

  1. 在RandomWordsState類中新增 _buildSuggestions()方法。這個類主要功能就是產生需要展示的單詞對列表。

Listview提供了builder屬性itemBuilder 用以產生item及匿名函式的回撥。BuildContext和行的迭代器索引 i ,這兩個引數被傳到ListView的buil()方法。迭代器索引從0開始增長,每次方法呼叫產生一個單詞對的時候就會增長。這就使得列表在使用者滾動時無限增長。
增加程式碼後,整個類看起來就是


 

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);

  final _saved = new Set<WordPair>();

  .....

  Widget _buildSuggestions() {
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        // The itemBuilder callback is called once per suggested word pairing,
        // and places each suggestion into a ListTile row.
        // For even rows, the function adds a ListTile row for the word pairing.
        // For odd rows, the function adds a Divider widget to visually
        // separate the entries. Note that the divider may be difficult
        // to see on smaller devices.
        itemBuilder: (context, i) {
          // Add a one-pixel-high divider widget before each row in theListView.
          if (i.isOdd) return new Divider();

          // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
          // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
          // This calculates the actual number of word pairings in the ListView,
          // minus the divider widgets.
          final index = i ~/ 2;
          // If you've reached the end of the available word pairings...
          if (index >= _suggestions.length) {
            // ...then generate 10 more and add them to the suggestions list.
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        }
    );
  }
}
  1. 在 _buildSuggestions()方法中呼叫了 _buildRow()方法。這個函式作用是在ListTile元件中顯示新的單詞對。ListTile元件可以讓你的每行看起來更加具有渲染力。

在RandomWordsState中新增 _buildRow()方法

class RandomWordsState extends State<RandomWords> {
  ...
  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}

  1. 最後來更新RandomWordsState入口函式build()。
    整體檔案最終程式碼:
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: "Weleocme to Flutter",
      home: new RandomWords(),
    );
  }

}



class RandomWords extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return new RandomWordsState ();
  }

}

class RandomWordsState extends State<RandomWords>{
  final _suggestions=<WordPair>[];
  final _biggerFont =const TextStyle(fontSize: 18.0);
  final _save=new Set<WordPair>();
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Startup Name Generator"),
      ),
    body: _buildSuggstions(),);
  }

  Widget _buildSuggstions(){
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        // The itemBuilder callback is called once per suggested word pairing,
        // and places each suggestion into a ListTile row.
        // For even rows, the function adds a ListTile row for the word pairing.
        // For odd rows, the function adds a Divider widget to visually
        // separate the entries. Note that the divider may be difficult
        // to see on smaller devices.
        itemBuilder: (context, i) {
          // Add a one-pixel-high divider widget before each row in theListView.
          if (i.isOdd) return new Divider();

          // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
          // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
          // This calculates the actual number of word pairings in the ListView,
          // minus the divider widgets.
          final index = i ~/ 2;
          // If you've reached the end of the available word pairings...
          if (index >= _suggestions.length) {
            // ...then generate 10 more and add them to the suggestions list.
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        }
    );


  }


  Widget _buildRow(WordPair pair) {
    return new ListTile(
        title:new Text(
          pair.asPascalCase,
          style: _biggerFont,
        ),);
  }

}

Step 5:新增互動

這步中,你講為每行item新增一個可點選的心形圖示,在使用者點選item時,對應的單詞對(word pair)會被新增到收藏或者被移除。

  1. 在RandomWordsState類中新增一個 _saved 集合(Set)變數。這個集合儲存了使用者喜歡並收藏的單詞對。之所以使用集合是因為集合可以保證其中沒有重複的單詞對。
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);

  final _saved = new Set<WordPair>();
  ...
}

  1. 在函式_buildRow()中,新增變數alreadySaved來檢查使用者點選的wordPair是否已經儲存。
  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    ...
  }

 

3.同樣還需要在函式_buildRow()中,需要在ListTiles中新增心形圖示來表示收藏狀態。後邊,你將會為次新增收藏取消功能的互動。

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,
      ),
    );
  }

重啟App。就將看到列表中每行右側添加了一個心形圖示。

為每行新增可點選功能。即若被點選的item對應的單詞對已經被收藏了,那麼就會被取消收藏,反之就新增到收藏。當一個tile被點選,函式呼叫setState()通知framework狀態發生改變。
新增的程式碼如下,在_buildRow()方法中進行新增

Tips:在Flutter的響應框架中,呼叫setState()方法會出發呼叫State類的bulid()方法,這就導致了更新UI操作。

重執行App。你應該可以通過點選來新增或者取消收藏。注意的一點是,在點選的時候可以看到一個放射性的點選效果,這是Material風格所致。如果有Android開發經驗的程式設計師就會知道。

Step 6:導向新的一屏

在這步中,你將新增一個新螢幕(在Flutter叫做route)來展示你的收藏。你將學習如何在home route和新route之間進行互動。

在Flutter中,導航器(Navigator)管理著所有app route的一個棧。向棧內push一個route就表示這將展示新的一屏。pop出棧表示向前顯示一屏。

在RandomWordsState類的build方法中,在AppBar上新增一個列表圖示。當用戶點選列表icon,包含收藏資料的新的route就會被推送的棧中,並且展示新的一屏。
 

>Tips:某些元件屬性只包含一個子元件(child),而某些屬性(像action)擁有一組子元件(children),這種方式通過方括號表示([])。

在方法中新增icon和對應的action:

....
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);

  final _saved = new Set<WordPair>();

  @override
  Widget build(BuildContext context) {
    return new Scaffold (
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[
          new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  ....
}

上邊的程式碼中同時定義了press回到,因此同時需要定義方法

class RandomWordsState extends State<RandomWords> {
  ...
  void _pushSaved() {
  }
}
  ...

此處方法中並未新增任何程式碼。

在使用者點選appbar上的列表icon。系統構建一個route並且push到Navigator的棧中。這種操作改變了螢幕的顯示,顯示了新的route。
這個新頁面的內容在MaterialPageRoute的builder的匿名函式中構建。

新增呼叫Navigator.push,將route推送的Navigator棧中。

...
class  RandomWordsState  extends  State<RandomWords>  {
  ...
  void  _pushSaved()  {
    Navigator.of(context).push(
    );
  }  
}
...

新增MaterialPageRoute和對應的builder。現在,可以在push方法中新增對應的程式碼,展示收藏的單詞對列表。使用toList()方法轉換將最終的資料賦值給divided 變數,使其擁有最終資料。

void _pushSaved() {  
  Navigator.of(context).push(new MaterialPageRoute(  
    builder: (context) {  
      final tiles = _saved.map(  
            (pair) {     
          return new ListTile(  
            title: new Text(  
              pair.asPascalCase,  
              style: _biggerFont,  
            ),  
          );  
         },  
      );  
      final divided = ListTile  
            .divideTiles(  
              context: context,  
              tiles: tiles,  
             )  
            .toList();  
      },  
    ),);  
}

builder屬性返回一個Scaffold(包含了新route的appbar)元件命名“Saved Suggestions”。新的route由ListTile元件組成的列表組成,ListTiles之間有分隔符分割。

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute(
      builder: (context) {
        final tiles = _saved.map(
          (pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();

        return new Scaffold(
          appBar: new AppBar(
            title: new Text('Saved Suggestions'),
          ),
          body: new ListView(children: divided),
        );
      },
    ),
  );
}

重執行App。收藏幾個單詞對,然後點選appbar上的列表icon,將會出現新的一屏來展示收藏的單詞對。注意新出現的一頁上預設會又給返回按鈕,這個Navigator預設新增的。這樣你不用特意為返回另外寫程式來執行Navigator.pop來返回。點選返回按鈕就可以返回到之前的頁面。
整體程式碼如下

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: "Weleocme to Flutter",
      home: new RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new RandomWordsState();
  }
}

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);
  final _saved = new Set<WordPair>();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Startup Name Generator"),
        actions: <Widget>[
          new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggstions(),
    );
  }

  Widget _buildSuggstions() {
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        // The itemBuilder callback is called once per suggested word pairing,
        // and places each suggestion into a ListTile row.
        // For even rows, the function adds a ListTile row for the word pairing.
        // For odd rows, the function adds a Divider widget to visually
        // separate the entries. Note that the divider may be difficult
        // to see on smaller devices.
        itemBuilder: (context, i) {
          // Add a one-pixel-high divider widget before each row in theListView.
          if (i.isOdd) return new Divider();

          // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
          // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
          // This calculates the actual number of word pairings in the ListView,
          // minus the divider widgets.
          final index = i ~/ 2;
          // If you've reached the end of the available word pairings...
          if (index >= _suggestions.length) {
            // ...then generate 10 more and add them to the suggestions list.
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final aleradySaved = _saved.contains(pair);
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: new Icon(
        aleradySaved ? Icons.favorite : Icons.favorite_border,
        color: aleradySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (aleradySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }

  void _pushSaved() {
    Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
      final titles = _saved.map((pair) {
        return new ListTile(
            title: new Text(
          pair.asPascalCase,
          style: _biggerFont,
        ));
      });
      final divided =
          ListTile.divideTiles(context: context, tiles: titles).toList();

      return new Scaffold(
        appBar: new AppBar(
          title: new Text("Saved Suggestions"),
        ),
        body: new ListView(children: divided),
      );
    }));
  }
}

Step 7:使用主題修改UI

這是最後一步,你將使用theme。主題(theme)主要控制app看起來外表如何。可以使用預設的主題,從文章開始到目前使用的一直是預設的主題。主題不依賴與物理裝置或者模擬器。你也可以自定義自己的主題來突顯你自己的品牌。

通過ThemeData類你可以很簡單的修改app的主題。
像下邊一樣修改下程式碼,就可以改變頁面的主標題主題

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      theme: new ThemeData(
        primaryColor: Colors.white,
      ),
      home: new RandomWords(),
    );
  }
}

重執行App看看效果。需要注意的是整個的頁面背景是白色的,甚至appbar也是白色的。

 

可以通過修改主題常量來檢視不同的效果,自己動手試試吧。