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會因為你寫的匯入語句給出建議。表現在你輸入的匯入語句會變成灰色,這就是提示你匯入的庫還沒有使用過。
- 使用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需要至少兩個類:
- StatefulWidget類用以建立例項;
- State類。
StatefulWidget元件本身是不可變的,但是State類在整個元件宣告週期過程中是始終存在的。
在這步中,你將會新增一個stateful widget,RandomWords以及建立對應的狀態State類,RandomWordsState。State類實際上為元件保留著喜歡的單詞對。
在main.dart檔案中新增RandomWords元件。這個類可以定義在檔案內任意位置,但是我將其定義在檔案末尾。
class RandomWords extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return new RandomWordsState ();
}
}
- 新增RandomWordsState類。大部分的app功能程式碼都會在這個類中。這個類同時儲存使用者滾動列表過程中產生的所有單詞對,以及使用者新增喜歡或者移除的單詞對。
下面新增對基本的了定義,來保證類檔案編譯通過 - 新增State類之後,IDE會提示缺少一個build()方法。下一步,需要將產生單詞對的程式碼移至這個build()方法中。
class RandomWordsState extends State<RandomWords>{
@override
Widget build(BuildContext context) {
final wordPair=new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
- 在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) {
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)會被新增到收藏或者被移除。
- 在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);
...
}
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也是白色的。
可以通過修改主題常量來檢視不同的效果,自己動手試試吧。