博客搬遷至https://blog.wangjiegulu.com
RSS訂閱:https://blog.wangjiegulu.com/feed.xml
以下代碼 Github 地址:https://github.com/wangjiegulu/flutter_test_01
編寫你的第一個Flutter App
這個你創建第一個Flutter app的指南。如果你熟悉面向對象的代碼,基本的編程概念,比如變量,循環,和條件,你就可以完成本教程。你不需要之前有Dart或者手機的編程經驗。
- 第1步:創建啟動 Flutter app
- 第2步:使用外部包
- 第3步:增加一個 Stateful widget
- 第4步:創建一個無限滾動的 ListView
- 第5步:增加交互
- 第6步:導航到一個新的頁面
- 第7步:使用 Theme 來改變 UI
- 干得不錯!
你將構建什么
你將要實現一個簡單的手機 app,為一個初創公司去生成一些推薦的名字。用戶可以選擇和取消選擇這些名字,並保存最好的一些名字。代碼一次生成10個名字。當用戶滾動時,新的一批名字就會被生成。用戶可以點擊 app bar 右上角的按鈕進入一個新的頁面來僅展示被喜歡的名字。
Gif 動圖展示了 app 完成之后的運行效果。

你將學到什么
- Flutter app 的基礎結構。
- 查詢和使用包來擴展特性。
- 使用熱重載來實現快速的開發周期。
- 怎么去實現一個 stateful widget 。
- 怎么去創建一個無限,懶加載的列表。
- 怎么去創建和導航到第二個頁面。
- 怎么去使用 Theme 來改變 app 的外觀。
你將使用什么
- Flutter SDK:Flutter SDK 包括 Flutter 的引擎,framework, widget ,工具和 Dart SDK。這個 codelab 需要 v0.1.4 或者更新。
- Android Studio IDE:這個 codelab 具備 Android Studio IDE,但是你也可以使用其它的 IDE,或者使用命令行工作。
- 你的 IDE 插件:你的 IDE 上面必須分別安裝 Flutter 和 Dart 插件。除了 Android Studio,Flutter 和 Dart 插件在 VS Code 和 IntelliJ IDE。
根據 開始你的第一個 Flutter app 的介紹,創建一個簡單,模版的 Flutter app。給項目取名為 startup_namer (替換掉 myapp)。您將修改這個 app 來創建完成的 app。
在這個 codelab 中,你主要編輯 dart 代碼存放處的 lib/main.dart。
提示:當復制代碼到你的 app 中,縮進可能會歪斜。你可以使用 Flutter 工具來自動修正它們:
- Android Studio / IntelliJ IDEA: 在 dart 代碼上右鍵並選擇 Reformat Code with dartfmt。
- VS Code: 右鍵並選擇 Format Document。
- Ternimal: 運行 flutter format
。
-
替換 lib/main.dart。
刪除 lib/main.dart 中的所有代碼。使用下面的代碼進行替換,它會在屏幕的中央展示 "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: 'Welcome to Flutter', home: new Scaffold( appBar: new AppBar( title: new Text('Welcome to Flutter'), ), body: new Center( child: new Text('Hello World'), ), ), ); } }
-
運行App,你將會看到如下的屏幕

觀察
- 這個例子創建了一個 Material app。Material 是手機和 web 上的標准的設計語言。Flutter 提供了豐富的 Material widget 。
- main 方法制定了一個寬箭頭(
=>
)標志,這是一行函數或者方法的簡寫。 - App 繼承了 StatelessWidget,這使得 app 本身稱為了一個 widget。在 Flutter 中,幾乎所有一切都是 widget,包括 alignment, padding, 和 layout。
- Material 庫中的 Scaffold,提供了一個默認的 app bar,title,和一個 body 屬性,它持有了主頁面的 widget 樹。widget 的子樹可能相當復雜。
- Widget 的主要的工作是提供一個
build()
方法,它描述了如何根據其他較低級別的 widget 顯示 widget。 - 這個例子中的 widget 樹的構成是一個中心的 widget 包含了一個文本的子 child widget。中心 widget 將它的 widget 子樹對齊到屏幕的中心。
## 第2步:使用外部包
在這一步,我將使用一個名為 english_words 的開源包,它包含了幾千個最常用的英文單詞和常用的工具方法。
在 pub.dartlang.org,你可以找到 english_words,以及很多其它的開源包。
-
pubspec 文件為 Flutter app 管理 assets。在 pubspec.yaml,增加 english_words (3.1.0或者更高) 到依賴列表。新增行在下面已被高亮:
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
-
在 Android Studio’s editor 視圖查看 pubspec,點擊右上角的 Packages get。這會把包拉取到你的項目中。你會在控制台上看到以下信息:
flutter packages get Running "flutter packages get" in startup_namer... Process finished with exit code 0
-
在 lib/main.dart,增加一個
english_words
的導入,就如高亮展示的那樣:import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart';
由於你的輸入,Android Studio 針對庫會給你一些導入的建議。然后將導入字符串呈現為灰色,讓你知道倒入的庫你沒有使用它(目前為止)。
-
使用 English words 包生成文本,用來替換掉之前的 "Hello World" 字符串。
提示:"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: 'Welcome to Flutter', home: new Scaffold( appBar: new AppBar( title: new Text('Welcome to Flutter'), ), body: new Center( //child: new Text('Hello World'), // Replace the highlighted text... child: new Text(wordPair.asPascalCase), // With this highlighted text. ), ), ); } }
-
如果 app 正在運行中,使用熱重載按鈕(
)來更新運行中的 app。每一次你點擊了熱重載,或者保存了項目,你將會看見不同的詞對,它在運行的 app 中是隨機的。這是因為詞對在 build 方法中被生成。在每次 MaterialApp 需要渲染或者在 Flutter Inspector 中切換平台的時候。

問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
- pubspec.yaml (pubspec.yaml 不會再次修改了。)
- lib/main.dart
## 第3步:增加一個 Stateful widget
Stateless widget 是不可改變的,意味着它們的屬性不能被修改 —— 所有值都是 final 的。
Stateful widgets 維護了狀態,它可能會在 widget 的生命周期內被修改。實現一個 statful widget 需要兩個類:1)一個 StatefulWidget 類,用來創建一個實例 2)一個 State 類。StatefulWidget 類本身是不可變的,但 State 類在整個 widget 的生命周期中保持不變。
在這一步中,你將會增加一個 stateful widget,RandomWords,增加它的 State class,RandomWordsState。State 類中將最終維護這個 widget 中推薦喜歡的詞對。
-
增加 stateful RandomWords widget 到你的 main.dart 中。它可以被放在任何地方,甚至 MyApp 之外,但是這里的解決方案放在了文件的底部。RandomWords widget 除了創建它的 State 類沒有什么特別的。
class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
-
增加 RandomWordsState 類。app 的大部分代碼將會寫在這個類中,它維護拉這個 widget 中的 state。這個類會保存生成的詞對,會被用戶無限滾動,用戶通過列表切換中的心圖標來添加或刪除它們。
你將逐步編寫這個類。作為開始,通過以下高亮的文本來創建一個最小的 class:
class RandomWordsState extends State<RandomWords> { }
-
在增加了 state class 之后,IDE 警告這個類缺少一個 build 方法。然后,你將增加一個基本的 build 方法通過從 MyApp 轉移生成詞對的代碼到 RandomWordsState 來生成詞對:
class RandomWordsState extends State<RandomWords> { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); return new Text(wordPair.asPascalCase); } }
-
通過以下高亮改變,從 MyApp 中移除生成詞對的代碼:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete this line 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(wordPair.asPascalCase), // Change the highlighted text to... child: new RandomWords(), // ... this highlighted text ), ), ); } }
重啟 app,如果你嘗試去熱重載,你可能會看到一個警告:
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.
這可能是誤報,但考慮重新啟動以確保你的更改反映在 app UI 中。
app 應該會跟以前一樣,每次你熱重載或者保存的時候展示一個詞對。

問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
## 第4步:創建一個無限滾動的 ListView
在這一步,你將擴展 RandomWordsState 來生成和展示一個列表的詞對。當用戶滾動時,展示在 ListView widget 的列表會無限滾動。ListView 的 builder
factory 構造方法允許你根據需要實現懶加載。
-
在 RandomWordsState 類中增加一個
_suggestions
list 來保存推薦的詞對。注意變量以下划線(_
)開頭。在 Dart 語言中,以下划線作為前綴標志代表私有。也增加一個
biggerFont
變量來使字體變大。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
在 RandomWordsState 類中增加一個
_buildSuggestions()
方法。這個方法構建展示詞對的 ListView。ListView 類提供了一個 builder 屬性,
itemBuilder
,以匿名方法的方式指定一個工廠構造器和回調方法。兩個參數會被傳入到方法中 —— BuildContext,和行迭代器,i
。迭代器從0開始,每一次方法被調用時遞增,每個推薦詞對配對一次。這個模型允許在用戶滾動時推薦列表無限滾動。增加以下高亮行:
class RandomWordsState extends State<RandomWords> { ... 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]); } ); } }
-
_buildSuggestions
方法在每個詞配對時調用。這個方法在一個 ListTile 中展示一個新的配對,在下一步中它允許你在行中增加交互。在
RandomWordsState
中增加一個_buildRow
方法:class RandomWordsState extends State<RandomWords> { ... Widget _buildRow(WordPair pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); } }
-
使用
_buildSuggestions()
來更新 RandomWordsState 的 build 方法,而不是直接調用生成詞對的庫。修改以下高亮改變:class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete these two lines. Return new Text(wordPair.asPascalCase); return new Scaffold ( appBar: new AppBar( title: new Text('Startup Name Generator'), ), body: _buildSuggestions(), ); } ... }
-
更新 MyApp 的 build 方法。在 MyApp 中移除 Scaffold 和 AppBar 實例。這些應該由 RandomWordsState 去管理,這讓在下一步中導航到另一個頁面時修改 app bar 的名字更簡單。
用下面高亮的 build 方法替換原生的方法:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Startup Name Generator', home: new RandomWords(), ); } }
重啟 app,你將看到一個詞對列表。按你想要的去滾動列表,你會看到新的詞對。

問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
## 第5步:增加交互
在這一步,你將在沒行增加一個可點擊的心型圖標。當用戶點擊 list 中的每行時,切換它的 “喜歡” 狀態,這會觸發詞對在保存的集合中增加或者刪除。
-
在 RandomWordsState 中增加一個
_saved
集合。這個集合存儲了用戶喜歡了的詞對。集合首選 List,因為正確的實現是 Set 不允許重復的條目。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
在
_buildRow
方法中,增加一個alreadySaved
檢查來確保詞對是否已經添加到喜歡集合中了。Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); ... }
-
在
_buildRow()
,增加一個心型的圖標到 ListTile 來啟用喜歡狀態。稍后,你會在這個心型圖標上增加一個交互。增加以下高亮:
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,現在你會看到每行都有心型圖標,但是它們還不能交互。
-
在
_buildRow
方法中讓心形圖標可點擊。如果一個詞對已經被添加到喜歡集合,再次點擊會從喜歡集合中刪除。當心形圖標被點擊,調用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, ), onTap: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); }
提示:在 Flutter 響應式風格框架中,調用 setState() 觸發 State 對象的 build() 方法的調用,結果更新在 UI 中。
熱重載 app,你應該會看到點擊任意行來喜歡,取消喜歡條目。注意,點擊一行會生成從心型圖標發出的隱式墨跡飛濺動畫。

問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
## 第6步:導航到新的頁面
在這一步,你將增加一個新的頁面(在 Flutter 被稱為 router)用來展示喜歡的集合。你將會學習到怎么從首頁導航到一個新的頁面。
在 Fluter,Navigator 管理包含了 app 路由頁面的棧。壓入一個頁面到 Navigator 的棧,更新展示那個頁面。從 Navigator 彈出一個頁面,返回展示上一個頁面。
-
在 RandomWordsState 的 build 方法中增加一個列表圖標到 AppBar 上。當用戶點擊這個列表圖標,一個包含了喜歡的條目的新頁面被壓入到 Navigator,展示圖標。
提示:一些widget屬性接收單個 widget(child),其它的屬性,如 action,接收一個數組 widgets(children),通過中括號([])標明。
在 build 方法中增加圖標和它對應的 action:
class RandomWordsState extends State<RandomWords> { ... @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(), ); } ... }
-
在 RandomWordsState 類中增加一個
_pushSaved()
方法。class RandomWordsState extends State<RandomWords> { ... void _pushSaved() { } }
熱重載 app,列表圖標出現在 app bar 上。點擊它還不會發生任何事情,因為
_pushSaved
方法是空的。 -
當用戶點擊 app bar 上的列表圖標,構建一個頁面並壓入 Navigator 的棧中。這個 action 會改變屏幕去展示新的頁面。
新頁面的內容在 MaterialPageRoute 的
builder
屬性中通過匿名方法構建。增加調用
Navigator.push
,如下高亮代碼展示,把頁面壓入到 Navigator 的棧里。void _pushSaved() { Navigator.of(context).push( ); }
-
增加 MaterialPageRoute 和它的 builder。現在,增加生成 ListTile 行的代碼。ListTile 的
divideTiles()
方法在每個 ListTile 之間添加水平間距。分割變量保存最后一行,由 convienice 函數toList()
轉換為列表。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,包含了新頁面的 app bar,名為 “Save Suggestions”。新頁面 body 的構成是一個 ListView 包含了 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,喜歡其中的一些條目並點擊 app bar 上的列表圖標。新頁面展示出來,且包含了喜歡的條目。注意 Navigator 在 app bar 上增加了一個 “Back” 按鈕。你不需要明確地實現 Navigator.pop。點擊返回按鈕來返回首頁。
問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
## 第7步:使用 Theme 來改變 UI
在這一步,你將玩轉 app 的 theme。Theme會控制你的 app 的視覺和感覺。你可以使用默認的 theme,這依賴於物理設備或者模擬器,或者你可以自定義 theme 來反映出你的品牌。
-
你可以很簡單地通過配置 ThemeData 類來改變 app 的主題。你的 app當前使用的是默認的主題,但是你將修改主要顏色為白色。
通過增加高亮的代碼到 MyApop 來改變 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,注意,整個背景都是白色的,甚至是 app bar。
-
作為讀者的聯系,使用 ThemeData 來改變 UI 的其它方面。Material 庫中的 Colors 類提供了很多顏色常量可以使用,然后熱重載使得 UI 實驗變的又快又簡單。
問題?
如果你的 app 沒有正確運行,排查錯誤。如果需要,請使用以下鏈接的代碼來追蹤。
## 干得不錯
你已寫了一個運行在 iOS 和 Android 的具有交互性的 Flutter app。在這個 codelab,你已經:
- 從頭創建了一個 Flutter app。
- 編寫 Dart 代碼。
- 使用外部第三方庫。
- 使用熱重載來進行快速的開發周期。
- 實現了 stateful widget,給你的 app 增加了互動性。
- 使用 ListView 和 ListTiles 創建了一個懶加載,無限滾動的列表。
- 創建了一個頁面,且增加了在主頁和新的頁面之前移動的邏輯。
- 學習改變 app 主題外觀和主題