從今天開始入坑Flutter,先從一個(gè)小小的ListView開始吧!
官方Codelabs:https://codelabs.flutter-io.cn/#codelabs
此處省略環(huán)境配置...
首先打開我們熟悉的Android Studio,創(chuàng)建Flutter工程
成功創(chuàng)建后,找到 lib/main.dart這個(gè)文件,把里邊的內(nèi)容刪除掉,替換成一下內(nèi)容:
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: const Text('Welcome to Flutter'),
),
body: const Center(
child: const Text('Hello World'),
),
),
);
}
}
在pubspec.yaml 中,將 english_words(3.1.0或更高版本)添加到依賴項(xiàng)列表
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
english_words: ^3.1.0 # 新增了這一行
在Android Studio 的編輯器視圖中查看 pubspec 時(shí),單擊右上角的 Packages get,這會(huì)將依賴包安裝到您的項(xiàng)目
在 lib/main.dart 中引入
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 新增了這一行
接下來,我們使用 English words 包生成文本來替換字符串"Hello World":
我們需要進(jìn)行如下更改:
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( // 這里把之前的 "const" 換成了 "new".
//child: const Text('Hello World'), // 我們不用這樣的方式生成文字了
child: new Text(wordPair.asPascalCase), // 這是新的文字生成方式
),
),
);
}
}
如果應(yīng)用程序正在運(yùn)行,請(qǐng)使用熱重載按鈕 (
build
方法內(nèi)部生成的。每次 MaterialApp 需要渲染時(shí)或者在 Flutter Inspector 中切換平臺(tái)時(shí) build
都會(huì)運(yùn)行.
接下來添加一個(gè) Stateful widget
Statelesswidgets 是不可變的,這意味著它們的屬性不能改變——所有的值都是 final。
Statefulwidgets 持有的狀態(tài)可能在 widget 生命周期中發(fā)生變化,實(shí)現(xiàn)一個(gè) stateful widget 至少需要兩個(gè)類:
1)一個(gè) StatefulWidget 類;
2)一個(gè) State 類,
StatefulWidget 類本身是不變的,但是 State 類在 widget 生命周期中始終存在。在這一步,你將添加一個(gè) stateful widget(有狀態(tài)的控件)—— RandomWords,它會(huì)創(chuàng)建自己的狀態(tài)類 —— RandomWordsState,然后你需要將 RandomWords 內(nèi)嵌到已有的無狀態(tài)的 MyApp widget。
創(chuàng)建一個(gè)最簡(jiǎn)的 state 類,這個(gè)類可以在任意地方創(chuàng)建而不一定非要在 MyApp 里,我們的示例代碼是放在 MyApp 類的最下面了:
class RandomWordsState extends State<RandomWords> {
// TODO Add build method
}
注意一下 State<RandomWords>
的聲明。這表明我們?cè)谑褂脤iT用于 RandomWords 的 State 泛型類。應(yīng)用的大部分邏輯和狀態(tài)都在這里 —— 它會(huì)維護(hù) RandomWords 控件的狀態(tài)。這個(gè)類會(huì)保存代碼生成的單詞對(duì),這個(gè)單詞對(duì)列表會(huì)隨著用戶滑動(dòng)而無限增長(zhǎng),另外還會(huì)保存用戶喜愛的單詞對(duì)(第二部分),也即當(dāng)用戶點(diǎn)擊愛心圖標(biāo)的時(shí)候會(huì)從喜愛的列表中添加或者移除當(dāng)前單詞對(duì)。
RandomWordsState 依賴 RandomWords,我們接下來會(huì)創(chuàng)建這個(gè)類。
添加有狀態(tài)的 RandomWords widget 到 main.dart,RandomWords widget 除了創(chuàng)建 State 類之外幾乎沒有其他任何東西:
class RandomWords extends StatefulWidget {
@override
RandomWordsState createState() => new RandomWordsState();
}
在添加狀態(tài)類后,IDE 會(huì)提示該類缺少 build 方法。接下來,您將添加一個(gè)基本的 build 方法,該方法通過將生成單詞對(duì)的代碼從 MyApp 移動(dòng)到 RandomWordsState 來生成單詞對(duì)。
將 build 方法添加到 RandomWordState 中,如下所示:
class RandomWordsState extends State<RandomWords> {
@override // 新增代碼片段 - 開始 ...
Widget build(BuildContext context) {
final WordPair wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
} // ... 新增的代碼片段 - 結(jié)束
}
如下所示,刪除 MyApp 里生成文字的代碼:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final WordPair 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), // 修改本行內(nèi)容
child: new RandomWords(), // 修改成本行代碼
),
),
);
}
}
熱重載(Hot reload)當(dāng)前的工程,應(yīng)用應(yīng)該像之前一樣運(yùn)行,每次熱重載或保存應(yīng)用程序時(shí)都會(huì)顯示一個(gè)單詞對(duì)。
接下來我們來創(chuàng)建一個(gè)無限滾動(dòng)的 ListView
向 RandomWordsState 類中添加一個(gè) _suggestions 列表以保存建議的單詞對(duì),同時(shí),添加一個(gè) biggerFont 變量來增大字體大小
提示:在 Dart 語言中使用下劃線前綴標(biāo)識(shí)符,會(huì)強(qiáng)制其變成私有。
class RandomWordsState extends State<RandomWords> {
// 添加如下兩行
final List<WordPair> _suggestions = <WordPair>[];
final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
...
}
接下來,我們將向 RandomWordsState 類添加一個(gè) _buildSuggestions() 函數(shù),此方法構(gòu)建顯示建議單詞對(duì)的 ListView。
ListView 類提供了一個(gè) builder 屬性,itemBuilder 值是一個(gè)匿名回調(diào)函數(shù), 接受兩個(gè)參數(shù)- BuildContext 和行迭代器 i。迭代器從 0 開始, 每調(diào)用一次該函數(shù),i 就會(huì)自增 1,對(duì)于每個(gè)建議的單詞對(duì)都會(huì)執(zhí)行一次。該模型允許建議的單詞對(duì)列表在用戶滾動(dòng)時(shí)無限增長(zhǎng)。
向 RandomWordsState 類添加 _buildSuggestions() 函數(shù),內(nèi)容如下:
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 對(duì)于每個(gè)建議的單詞對(duì)都會(huì)調(diào)用一次 itemBuilder,
// 然后將單詞對(duì)添加到 ListTile 行中
// 在偶數(shù)行,該函數(shù)會(huì)為單詞對(duì)添加一個(gè) ListTile row.
// 在奇數(shù)行,該函數(shù)會(huì)添加一個(gè)分割線的 widget,來分隔相鄰的詞對(duì)。
// 注意,在小屏幕上,分割線看起來可能比較吃力。
itemBuilder: (BuildContext _context, int i) {
// 在每一列之前,添加一個(gè)1像素高的分隔線widget
if (i.isOdd) {
return new Divider();
}
// 語法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整)
// 比如 i 為:1, 2, 3, 4, 5 時(shí),結(jié)果為 0, 1, 1, 2, 2,
// 這可以計(jì)算出 ListView 中減去分隔線后的實(shí)際單詞對(duì)數(shù)量
final int index = i ~/ 2;
// 如果是建議列表中最后一個(gè)單詞對(duì)
if (index >= _suggestions.length) {
// ...接著再生成10個(gè)單詞對(duì),然后添加到建議列表
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
對(duì)于每一個(gè)單詞對(duì),_buildSuggestions 函數(shù)都會(huì)調(diào)用一次 _buildRow。 這個(gè)函數(shù)在 ListTile 中顯示每個(gè)新詞對(duì),這使您在下一步中可以生成更漂亮的顯示行。
在 RandomWordsState 中添加 _buildRow 函數(shù) :
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
更新 RandomWordsState
的 build
方法以使用 _buildSuggestions()
,而不是直接調(diào)用單詞生成庫,代碼更改后如下:(使用 Scaffold 類實(shí)現(xiàn)基礎(chǔ)的 Material Design 布局)
@override
Widget build(BuildContext context) {
//final wordPair = new WordPair.random(); // 刪掉 ...
//return new Text(wordPair.asPascalCase); // ... 這兩行
return new Scaffold ( // 代碼從這里...
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
); // ... 添加到這里
}
將 build() 方法添加到 MyApp 類中,如下所示:
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
重新啟動(dòng)你的項(xiàng)目工程應(yīng)用,你應(yīng)該看到一個(gè)單詞對(duì)列表。盡可能地向下滾動(dòng),你將繼續(xù)看到新的單詞對(duì)。
向列表里添加圖標(biāo)
添加一個(gè) _saved Set(集合)到 RandomWordsState,這個(gè)集合存儲(chǔ)用戶喜歡(收藏)的單詞對(duì)。 在這里,Set 比 List 更合適,因?yàn)?Set 中不允許重復(fù)的值。
class RandomWordsState extends State<RandomWords> {
final List<WordPair> _suggestions = <WordPair>[];
final Set<WordPair> _saved = new Set<WordPair>(); // 新增本行
final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
...
}
在 _buildRow 方法中添加 alreadySaved 來檢查確保單詞對(duì)還沒有添加到收藏夾中。
Widget _buildRow(WordPair pair) {
final bool alreadySaved = _saved.contains(pair); // 新增本行
...
}
同時(shí)在 _buildRow() 中, 添加一個(gè)心形 ??圖標(biāo)到 ListTiles以啟用收藏功能。接下來,你就可以給心形 ??圖標(biāo)添加交互能力了。
向列表添加圖標(biāo),如下所示:
Widget _buildRow(WordPair pair) {
final bool 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,
), // ... 新增代碼結(jié)束
);
}
熱重載應(yīng)用,你現(xiàn)在可以在每一行看到心形 ??圖標(biāo)?,但它們還沒有交互。
在這部分,我們將為剛剛的心形 ??圖標(biāo)增加交互,當(dāng)用戶點(diǎn)擊列表中的條目,切換其"收藏"狀態(tài),并將該詞對(duì)添加到或移除出"收藏夾"。
為了做到這個(gè),我們?cè)?_buildRow 中讓心形 ??圖標(biāo)變得可以點(diǎn)擊。如果單詞條目已經(jīng)添加到收藏夾中, 再次點(diǎn)擊它將其從收藏夾中刪除。當(dāng)心形 ??圖標(biāo)被點(diǎn)擊時(shí),函數(shù)調(diào)用 setState() 通知框架狀態(tài)已經(jīng)改變。
增加 onTap 方法,如下所示:
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: () { // 增加如下 9 行代碼...
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
}, // ... 一直到這里
);
}
提示: 在 Flutter 的響應(yīng)式風(fēng)格的框架中,調(diào)用 setState() 會(huì)為 State 對(duì)象觸發(fā) build() 方法,從而導(dǎo)致對(duì) UI 的更新
熱重載應(yīng)用,你就可以點(diǎn)擊任何一行測(cè)試收藏或取消收藏功能,你的點(diǎn)擊同時(shí)自帶 Material Design 里的水波動(dòng)畫特效。
導(dǎo)航到新頁面
在這一步中,您將添加一個(gè)顯示收藏夾內(nèi)容的新頁面(在 Flutter 中稱為路由[route])。您將學(xué)習(xí)如何在主路由和新路由之間導(dǎo)航(切換頁面)。
在 Flutter 中,導(dǎo)航器管理應(yīng)用程序的路由棧。將路由推入(push)到導(dǎo)航器的棧中,將會(huì)顯示更新為該路由頁面。 從導(dǎo)航器的棧中彈出(pop)路由,將顯示返回到前一個(gè)路由。
接下來,我們?cè)?RandomWordsState 的 build 方法中為 AppBar 添加一個(gè)列表圖標(biāo)。當(dāng)用戶點(diǎn)擊列表圖標(biāo)時(shí),包含收藏夾的新路由頁面入棧顯示。
將該圖標(biāo)及其相應(yīng)的操作添加到 build 方法中:
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: const Icon(Icons.list), onPressed: _pushSaved),
], // ... 代碼新增結(jié)束
),
body: _buildSuggestions(),
);
}
...
}
提示: 某些 widget 屬性需要單個(gè) widget(child),而其它一些屬性,如 action,需要一組widgets(children),用方括號(hào) [] 表示。
在 RandomWordsState 這個(gè)類里添加 _pushSaved() 方法:
class RandomWordsState extends State<RandomWords> {
...
// 新增代碼開始
void _pushSaved() {
}
// 新增代碼結(jié)束
}
熱重載應(yīng)用,列表圖標(biāo)()將會(huì)出現(xiàn)在導(dǎo)航欄中。現(xiàn)在點(diǎn)擊它不會(huì)有任何反應(yīng),因?yàn)?_pushSaved
函數(shù)還是空的。
接下來,(當(dāng)用戶點(diǎn)擊導(dǎo)航欄中的列表圖標(biāo)時(shí))我們會(huì)建立一個(gè)路由并將其推入到導(dǎo)航管理器棧中。此操作會(huì)切換頁面以顯示新路由,新頁面的內(nèi)容會(huì)在 MaterialPageRoute 的 builder 屬性中構(gòu)建,builder 是一個(gè)匿名函數(shù)。
添加 Navigator.push 調(diào)用,這會(huì)使路由入棧(以后路由入棧均指推入到導(dǎo)航管理器的棧)
void _pushSaved() {
Navigator.of(context).push(
);
}
接下來,添加 MaterialPageRoute 及其 builder。 現(xiàn)在,添加生成 ListTile 行的代碼,ListTile 的 divideTiles() 方法在每個(gè) ListTile 之間添加 1 像素的分割線。 該 divided 變量持有最終的列表項(xiàng),并通過 toList()方法非常方便的轉(zhuǎn)換成列表顯示。
添加如下所示的代碼:
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute<void>( // 新增如下20行代碼 ...
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
},
), // ... 新增代碼結(jié)束
);
}
builder 返回一個(gè) Scaffold,其中包含名為"Saved Suggestions"的新路由的應(yīng)用欄。新路由的body 由包含 ListTiles 行的 ListView 組成;每行之間通過一個(gè)分隔線分隔。
添加水平分隔符,如下代碼所示:
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold( // 新增 6 行代碼開始 ...
appBar: new AppBar(
title: const Text('Saved Suggestions'),
),
body: new ListView(children: divided),
); // ... 新增代碼段結(jié)束.
},
),
);
}
熱重載應(yīng)用程序,點(diǎn)擊列表項(xiàng)收藏一些項(xiàng),點(diǎn)擊列表圖標(biāo)(),在新的 route(路由)頁面中顯示收藏的內(nèi)容。Navigator(導(dǎo)航器)會(huì)在應(yīng)用欄中自動(dòng)添加一個(gè)"返回"按鈕,無需調(diào)用Navigator.pop
,點(diǎn)擊后退按鈕就會(huì)返回到主頁路由。
使用 Themes 修改 UI
這一部分,我們將會(huì)一起修改應(yīng)用的主題。Flutter 里我們使用 theme 來控制你應(yīng)用的外觀和風(fēng)格,你可以使用默認(rèn)主題,該主題取決于物理設(shè)備或模擬器,也可以自定義主題以適應(yīng)您的品牌。
你可以通過配置 ThemeData 類輕松更改應(yīng)用程序的主題,目前我們的應(yīng)用程序使用默認(rèn)主題,下面將更改 primaryColor 顏色為白色。
在 MyApp 這個(gè)類里修改顏色:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData( // 新增代碼開始...
primaryColor: Colors.white,
), // ... 代碼新增結(jié)束
home: new RandomWords(),
);
}
}
熱重載應(yīng)用。 你會(huì)發(fā)現(xiàn),整個(gè)背景將會(huì)變?yōu)榘咨?app bar(應(yīng)用欄)。
一個(gè)小練習(xí),你可以看一下 ThemeData 的文檔,添加其他屬性來更多改變 UI 樣式。Material library 中的 Colors 類提供了許多可以使用的顏色常量, 你可以使用熱重載來快速簡(jiǎn)單地嘗試、實(shí)驗(yàn)。
以上完成了一個(gè)ListView