Flutter(二) 建立第一個Flutter App
這一章主要是建立一個Flutter App。如果你熟悉面向物件程式設計,有基本的程式設計概念(變數,迴圈,條件判斷等),那麼你不必要具備原有的Dart和移動開發經驗,,就可以輕鬆地理解完成這章內容。
構造什麼
你將為一家初創公司實現一個簡單的移動app,主要功能是為這家公司推薦名字。使用者可以選擇或者取消名字,儲存最好的名字。程式會一次性產生10個名字。在使用者滾動時,新名字同時會產生出來。使用者可以點選導航欄(appbar)的列表圖示進入到一個新的列表頁檢視喜歡的名字。
最終的結果最後執行結果中可以看到。
你將會學到:
- Flutter app基本結構
- 查詢並使用包來擴充套件功能
- 使用熱載入加快開發週期
- 如何實現一個有狀態元件(Stateful widget)
- 如何建立一個無限,懶載入列表
- 如何建立並路由到第二個螢幕
- 如何使用app主題修改外觀
你將會使用:
需要安裝:
- Flutter SDK
Flutter SDK包含了Flutter引擎,framework,元件,工具,和Dart SDK。這份程式碼實驗需要v0.1.4或者更高版本。
Android Studio
這次程式碼實驗需要Android Studio。也可以在命令列。需要IDE外掛
IDE上必須分別安裝Flutter和Dart外掛。檢視Flutter安裝學習如何建立起你的Flutter環境
Step 1:建立Flutter App
建立第一個簡單的,IDE提供模板的Flutter App,可以
在這節程式實驗中,最多編輯的會是lib/main.dart,其中就是Dart程式碼。
Tips:當你貼上程式碼到IDE中的時候,可能發生縮排不對齊的情況。你可以使用Flutter工具來解決這種問題: 1. Android Studio/IntelliJ IDEA:右單擊Dart程式碼,選擇Refactor code with dartfmt 2. VS Code:右單擊選擇 Format Document 3. Terminal:執行命令 flutter format filename |
- 替換模板的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: '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元件。
- main()方法聲明瞭胖箭頭(=>)符號表示法,這種寫法表示了man()方法是一個單行函式,即函式體只有一行程式碼構成。
- App繼承StatelessWidget,這也使得App本身成為了一個元件。在Flutter中,幾乎所有物件都被認為是一個元件,包含對齊方式,內邊距和佈局。
- Material元件庫中的Scaffold提供了App預設需要的appbar,title,和body屬性,body屬性包含了home screen的元件樹結構。元件的子元件結構可以相當複雜。
- 元件的主要工作就是提供build()方法,描述如何展示自己及其他元件。
- 這個例子中元件結構有一個Center元件中包含一個Text子元件組成。Center元件將其內的元件結構置於螢幕中央。
Step 2:使用外部包
這一步中,你將使用開源包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.0
english_words: ^3.1.0
...
原有模板檔案中程式碼太多(包含註釋),不全部展示出來。這裡在depenencies下新增english_words包依賴。
在Android Studio編輯器中檢視pubspec.yaml檔案時,可以看到編輯器右上方有命令操作欄,
點選Package get。這就是獲取english_words包操作。你會在控制檯命令列看到:
在lib/main.dart中,新增對english_words包的匯入,如下顯示的匯入語句:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
在你輸入後,AS會因為你寫的匯入語句給出建議。表現在你輸入的匯入語句會變成灰色,這就是提示你匯入的庫還沒有使用過。
- 使用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: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome 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
createState() => new RandomWordsState();
}
- 新增RandomWordsState類。大部分的app功能程式碼都會在這個類中。這個類同時儲存使用者滾動列表過程中產生的所有單詞對,以及使用者新增喜歡或者移除的單詞對。
下面新增對基本的了定義,來保證類檔案編譯通過
class RandomWordsState extends State<RandomWords> {
}
- 新增State類之後,IDE會提示缺少一個build()方法。下一步,需要將產生單詞對的程式碼移至這個build()方法中。
在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工廠方法可以按需要進行懶載入。
- 在RandomWordsState中新增 _suggestions 列表變數儲存生成的候選單詞對。變數以 (_) 開頭 ——在Dart中,下劃線開頭的變數強調私有
同時新增比變數 biggerFont 改變字型大小。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
...
}
- 在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]);
}
);
}
}
- 在 _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,
),
);
}
}
- 最後來更新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 RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
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]);
}
);
}
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
}
- 最終重新啟動App執行。
Step 5:新增互動
這步中,你講為每行item新增一個可點選的心形圖示,在使用者點選item時,對應的單詞對(word pair)會被新增到收藏或者被移除。
- 在RandomWordsState類中新增一個 _saved 集合(Set)變數。這個集合儲存了使用者喜歡並收藏的單詞對。之所以使用集合是因為集合可以保證其中沒有重複的單詞對。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
final _saved = new Set<WordPair>();
...
}
- 在函式_buildRow()中,新增變數alreadySaved來檢查使用者點選的wordPair是否已經儲存。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}
- 同樣還需要在函式_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()方法中進行新增
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);
}
});
},
);
}
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) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => 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: _buildSuggestions(),
);
}
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),
);
},
),
);
}
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]);
});
}
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);
}
});
},
);
}
}
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也是白色的。
你可以通過修改引用來改變主題樣式,自己試試吧。
好了,這章的內容就是這些了。