在 上一篇文章 中,我們講解了 Flutter 開(kāi)發(fā)環(huán)境搭建 , 以及運(yùn)行了官方demo簡(jiǎn)單體驗(yàn)了下 Flutter app .
此篇我們將開(kāi)始對(duì)一些 Flutter app 中的一些概念做一些講解 , 一些基本的操作做一些示例 , 主要是參考官網(wǎng)的教程 Write Your First Flutter App
若你對(duì)面向?qū)ο缶幊淌煜?以及對(duì)基本編程概念如變量够庙、循環(huán)、條件了解 , 則適合閱讀本篇教程 . 不必需要擁有 Dart 或移動(dòng)編程經(jīng)驗(yàn).
為了更好的閱讀體驗(yàn) , 請(qǐng)點(diǎn)擊 閱讀原文 :)
- 步驟 1 : 創(chuàng)建及啟動(dòng) Flutter app
- 步驟 2 : 使用一個(gè)外部的程序包
- 步驟 3 : 增加一個(gè) Stateful Widget
- 步驟 4 : 創(chuàng)建一個(gè)無(wú)限滾動(dòng)的 ListView
- 步驟 5 : 增加交互
- 步驟 6 : 跳轉(zhuǎn)到新頁(yè)面
- 步驟 7 : 通過(guò)主題改變UI
- 完成!
我們將創(chuàng)建什么
我們將實(shí)現(xiàn)一個(gè)簡(jiǎn)單的移動(dòng)應(yīng)用 , 它會(huì)生成創(chuàng)業(yè)公司的名稱(chēng) . 用戶(hù)可以選擇和反選名稱(chēng) , 保存最好的那些 . 代碼一次生成 10 個(gè)名稱(chēng) . 當(dāng)用戶(hù)滑動(dòng)時(shí) , 新一批的名稱(chēng)就會(huì)生成 . 用戶(hù)可以點(diǎn)擊導(dǎo)航欄右上的按鈕進(jìn)入一個(gè)只展示喜好的名稱(chēng)的列表新頁(yè)面.
我們將學(xué)到:
- Flutter app 的基本結(jié)構(gòu)
- 使用額外的包去拓展功能
- 使用熱部署來(lái)快速開(kāi)發(fā)
- 如何去實(shí)現(xiàn)一個(gè)stateful 小部件
- 如何創(chuàng)建一個(gè)無(wú)線滑動(dòng),懶加載的列表
- 如何跳轉(zhuǎn)去下一個(gè)界面
- 如果通過(guò)主題去修改app外觀
步驟 1 : 創(chuàng)建及啟動(dòng) Flutter app
這里創(chuàng)建一個(gè)簡(jiǎn)單的 flutter app
flutter create flutter_first_app
cd flutter_first_app
flutter run
如有疑問(wèn) , 可參考 前一篇文章 指引
簡(jiǎn)單地 , 我們先將 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'),
),
),
);
}
}
重新運(yùn)行得到結(jié)果
發(fā)現(xiàn)
- 這個(gè)例子創(chuàng)建了一個(gè) Material Design 風(fēng)格的app . Material 是一種在移動(dòng)端及web上標(biāo)準(zhǔn)的視覺(jué)設(shè)計(jì)語(yǔ)言 . Flutter 提供了豐富的 Material 風(fēng)格小部件
- main 方法使用了一個(gè)大箭頭
=>
寫(xiě)法 , 它是一行代碼功能或方法的縮寫(xiě) - app 繼承 StatefulWidget 使得其自身也是個(gè)widget . 在 Flutter 里 , 大多數(shù)元素都是 widget , 包括對(duì)齊方式(alignment)蕾各、 內(nèi)邊距(padding)度帮、布局(layout) .
- Material 庫(kù) 中的腳手架小部件 (Scaffold widget) , 提供了一個(gè)默認(rèn)的導(dǎo)航欄、 標(biāo)題、 內(nèi)容屬性在屏幕中維持了部件樹(shù)??.部件子樹(shù)可以是很復(fù)雜的.
- 一個(gè)小部件的主要工作就是提供
build()
方法 , 它是用來(lái)表明如何展示其他低層級(jí)的widget. - 這個(gè)示例的部件樹(shù)由 包含一個(gè) Text 子部件 的Center Widget 組成 . 這個(gè) Center Widget 將其子部件樹(shù)排列在屏幕中間 .
步驟 2 : 使用一個(gè)外部的程序包
在這個(gè)步驟里 , 我們將開(kāi)始使用一個(gè)開(kāi)源程序包 english_words , 它包含了較多的常用的英文單詞還有一些工具方法 .
我們可以在 pub.dartlang.org 找到 english_words 及 其他開(kāi)源程序包
1. pubspec 文件負(fù)責(zé)管理 Flutter 應(yīng)用的資源. 在 pubspec.yaml 文件中,添加 english_words(3.1.0或更高版本)到依賴(lài)?yán)?
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
english_words: ^3.1.0
**2. **當(dāng)我們?cè)贗DEA 視圖中 , 修改yaml文件后 , 可點(diǎn)擊右上方的 Packages get 使之生效.它會(huì)拉取我們才添加的依賴(lài)包, 控制臺(tái)中打印
flutter packages get
Running "flutter packages get" in flutter_first_app...
Process finished with exit code 0
**3. ** 在 lib/main.dart文件中,添加 import 語(yǔ)句 , 導(dǎo)入依賴(lài)相關(guān)類(lèi)
import 'package:english_words/english_words.dart';
**4. ** 用開(kāi)源庫(kù)生成文本代替原來(lái)的 'Hello World'
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.
),
),
);
}
}
**5. **若app正在運(yùn)行, 可以通過(guò)點(diǎn)擊??按鈕進(jìn)行熱部署. 每次點(diǎn)擊或者保存時(shí) , 會(huì)生成新的一個(gè)隨機(jī)單詞. 這是因?yàn)閱卧~是在 build(...)
方法中生成, 它會(huì)在每次 MaterialApp 需要渲染或觸發(fā)平臺(tái)檢視時(shí)執(zhí)行.
步驟 3 : 增加一個(gè) Stateful Widget
Stateless widgets 是不可變的 , 意味著其屬性是不可改變的 - 所有值均為final .
Stateful widgets 維持著生命周期中可變的狀態(tài) . 實(shí)現(xiàn)一個(gè) stateful widget 需要至少兩個(gè)類(lèi): 一個(gè) State 類(lèi) 和 一個(gè)創(chuàng)建State示例的 StatefulWidget . StatefulWidget本身是不可變的 , 但是 State 類(lèi)在widget生成周期中一直存留 .
在這個(gè)步驟里 , 我們將添加一個(gè) stateful widget - RandomWords , 它創(chuàng)建自己的 State 類(lèi) - RandomWordsState . state 類(lèi)將為widget最終維持建議的和喜好的單詞.
**1. ** 添加 stateful RandomWords 到 main.dart
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
**2. ** 添加 RandomWordsState . 大部分app的代碼會(huì)在這個(gè)類(lèi)中 , 將維持著 RandomWords 部件的狀態(tài) . 這個(gè)類(lèi)將會(huì)保存生成的詞對(duì) , 它們隨著用戶(hù)滑動(dòng)頁(yè)面無(wú)線增加 . 然后喜好的詞對(duì) , 用戶(hù)通過(guò)點(diǎn)擊列表的心形按鈕進(jìn)行添加或移除 .
我們一步一步來(lái)創(chuàng)建這個(gè)類(lèi)
class RandomWordsState extends State<RandomWords> {
}
**3. ** 在添加 state 類(lèi)后 , IDE會(huì)提示錯(cuò)誤, 需要我們?nèi)?shí)現(xiàn)未實(shí)現(xiàn)的方法 .
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
**4. ** 移除單詞生成代碼 ,
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(),
),
),
);
}
}
步驟 4 : 創(chuàng)建一個(gè)無(wú)限滾動(dòng)的 ListView
這個(gè)步驟里, 我們將擴(kuò)充 RandomWordsState 類(lèi) 來(lái)生成和展示單詞對(duì)的列表. 當(dāng)用戶(hù)滑動(dòng)頁(yè)面, ListView widget 展示的列表將會(huì)無(wú)限增加. ListView 的 builder
工廠構(gòu)造器允許我們視需懶加載創(chuàng)建列表視圖
**1. **在 RandomWordsState 類(lèi)中添加成員變量 _suggestions
列表用來(lái)保存推薦的單詞對(duì).在 Dart 語(yǔ)言中 , 以 _
下劃線開(kāi)頭的變量/方法為私有訪問(wèn)權(quán)限.
同樣的 , 添加 biggerFont
變量用來(lái)使字體大小更大
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
...
}
**2 ~ 3. **添加 _buildSuggestions()
方法到 RandomWordsState 類(lèi)中. 此方法負(fù)責(zé)構(gòu)建 ListView 和展示建議的單詞對(duì).
ListView 類(lèi)提供了一個(gè) builder 屬性 , itemBuilder
一個(gè)工廠構(gòu)建者和指定匿名函數(shù)的回調(diào)功能.兩個(gè)參數(shù)被傳遞給函數(shù)- BuildContext
還有行迭代器 i
. 迭代器從 0
開(kāi)始且每次方法調(diào)用遞增.
添加 _buildRow
方法
Widget _buildSuggestions() {
return new ListView.builder(
// padding 16
padding: const EdgeInsets.all(16.0),
// 每一對(duì)單詞對(duì)調(diào)用一次itemBuilder 回調(diào) ,然后放置一個(gè)推薦的單詞對(duì)在行內(nèi)
// 偶數(shù)行 , 函數(shù)增加個(gè)內(nèi)容行顯示單詞對(duì),
// 奇數(shù)行 , 函數(shù)添加一條分割線小部件 (Divider Widget)去顯示分割條目
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
// index 為 i/2 的余整數(shù)
final index = i ~/ 2;
if (index >= _suggestions.length) {
// ...生成10個(gè)詞對(duì),添加到list
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
**4. **更新 RandomWordsState 的 build 方法 , 使用 _buildSuggestions
,不再直接使用調(diào)用單詞生產(chǎn)庫(kù).
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
...
}
**5. **更新 MyApp 的 build 方法 . 從 MyApp 中移除 Scaffold 和 AppBar 實(shí)例. 這些將會(huì)被 RandomWordsState 管理 ,這樣將更容易地在下個(gè)步驟頁(yè)面跳轉(zhuǎn)時(shí)去改變導(dǎo)航欄上的名稱(chēng).
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
步驟 5 : 增加交互
這個(gè)步驟中 , 我們將增加可點(diǎn)擊的心形圖標(biāo)到每一行 . 當(dāng)用戶(hù)點(diǎn)擊列表?xiàng)l目時(shí) , 觸發(fā)其 "favorite" 狀態(tài) ,狀態(tài)改變會(huì)將對(duì)應(yīng)單詞對(duì)添加到保存的集合或從中移除
**1. ** 添加 _saved
集合到 RandomWordsState 里 . 集合存儲(chǔ)用戶(hù)喜好的單詞對(duì) , 更傾向于用 Set 是因?yàn)?Set 中不允許有重復(fù)的條目
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
...
}
**2. **在 _buildRow
函數(shù)里 , 添加 alreadySaved
變量來(lái)檢查確保單詞對(duì)還未被添加到喜好的集合中.
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}
**3 ~ 4. **還是在_buildRow
里 , 添加心形圖標(biāo) . 重啟應(yīng)用 , 我們可以看到心形已被添加 , 只是暫時(shí)沒(méi)有交互事件
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,
),
);
}
**5. **在_buildRow
中設(shè)置心形可點(diǎn)擊. 如果一個(gè)單詞條目被添加到喜歡的集合時(shí), 再次單擊它就能從喜歡的集合中移除 . 當(dāng)心形被點(diǎn)擊 , 函數(shù)會(huì)調(diào)用 setState()
去通知框架狀態(tài)被改變了.
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 react式的框架中 , 調(diào)用 setState() 會(huì)為 State 對(duì)象觸發(fā) build() 方法 , 最后更新到UI上.
步驟 6 : 跳轉(zhuǎn)到新頁(yè)面
在這個(gè)步驟里 , 我們將添加一個(gè)頁(yè)面 (在Flutter里叫 route ) 展示喜好的推薦詞對(duì) . 我們將學(xué)到如何從主頁(yè)面導(dǎo)航到新頁(yè)面 .
在 Flutter 中 , Navigator 管理著一個(gè)包含app頁(yè)面的棧 . 推送一個(gè)頁(yè)面進(jìn)入 Navigator 的棧中, 則會(huì)更新顯示這個(gè)頁(yè)面 . 從 Navigator棧中推出一個(gè)頁(yè)面 , 則會(huì)顯示上一個(gè)頁(yè)面 .
**1 ~ 3. ** 在 RandomWordsState 的 build 方法中給 AppBar 添加一個(gè)列表圖標(biāo) . 當(dāng)用戶(hù)點(diǎn)擊圖標(biāo) , 一個(gè)包含喜好列表的頁(yè)面會(huì)被推送呈現(xiàn) .
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() {
}
}
**4 ~ 6. ** 添加 MaterialPageRoute 及它的 builder . 添加代碼生成 ListTile 行 . ListTile 的divideTiles()
方法在每一條條目間增加水平距離 . divided
變量保存著最終的行 , 通過(guò)函數(shù) toList()
轉(zhuǎn)換為列表
builder 屬性返回一個(gè) Scaffold , 包含了新頁(yè)面的導(dǎo)航欄 ,名為 "Saved Suggestion" .新頁(yè)面的內(nèi)容部分由 ListView包含 ListTiles 行組成 , 每一行由一個(gè) divider 分隔 .
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();
},
),
);
}
步驟 7 : 通過(guò)主題改變UI
在這個(gè)最終步驟中, 我們將改變app的主題 .
**1. **我們可以簡(jiǎn)單地通過(guò)配置 ThemeData 類(lèi) 改變app的主題 . 當(dāng)前app是默認(rèn)主題, 我們將改變主色為紫色
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
theme: new ThemeData(
primaryColor: Colors.purple
),
);
}
}
完成!
至此 , 我們第一個(gè) app 已經(jīng)完成 . GitHub 地址
功能相對(duì)來(lái)說(shuō)較簡(jiǎn)單 , 但是大體上讓我們對(duì)開(kāi)發(fā) Flutter app 有了一定了解. 之后我們將延續(xù)閱讀官網(wǎng)的教程 , 開(kāi)始較全面地了解構(gòu)建UI相關(guān)的部分.