原文連結
前言
學習Flutter你一定會看到官網的第一個例子:中文版 或 英文版。但是作為新手,或許你看的會很費勁,這篇文章的目的是幫助你更好的理解這個例子。
最終的效果圖:
我們先分析一下如何實現上圖中的效果:
Android開發者
1. 準備資料:列表資料和選中的資料可以分別使用兩個List或者陣列儲存。 2. 介面列表:使用ListView或RecyclerView 3. 介面跳轉:可以使用Intent攜帶資料到新的列表頁 |
iOS開發者
1. 準備資料:列表資料和選中的資料可以分別使用兩個陣列儲存。 2. 介面列表:使用TableView或CollectionView 3. 介面跳轉:使用NavigationController,可以把值直接賦值給新的頁面物件 |
結論
我們發現,無論是原生的Android還是iOS開發,都需要做的步驟是:
- 儲存要展示的資料,儲存選中的資料
- 展示列表,並把資料展示出來
- 設定跳轉到新頁面
所以在Flutter開發中,也遵照這幾個步驟會更好的理解
Flutter開發
* 準備資料:列表資料使用陣列儲存,選中的資料可以使用Set儲存(因為set可以自動去重)。 * 介面列表:使用ListView * 介面跳轉:可以使用Navigator |
拆解分析官方程式碼,帶你快速理解
官網上使用大概110行程式碼實現上面的例子,我們把這些程式碼拆解成主要的三部分來幫助我們學習:
前提:你首先應該會用Android studio或者其他開發工具建立一個Flutter的工程,如果你需要學習關於這個步驟,可以在 這裡快速學習
當你建立一個全新的Flutter工程並執行,介面上會出現熟悉的“Hello world”。 為了更容易的理解Flutter的程式碼,我們先分析一下建立初始的程式碼,至少要知道我們需要從哪裡開始動手:
我們要編輯的就是這裡的 main.dart 檔案,跟其他語言一樣,Flutter的入口函式是main函式:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp()); //分析 1
class MyApp extends StatelessWidget { //分析 2 (StatelessWidget)
@override
Widget build(BuildContext context) { //分析 3
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold( //分析 4
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center( //分析 5
child: new Text('Hello World'),
),
),
);
}
}
複製程式碼
分析
- 這裡的 => 是Dart中單行函式的簡寫,等價於:
void main() {
runApp(new MyApp());
}
複製程式碼
-
StatelessWidget 代表只有一種狀態的元件,與之對應的是StatefulWidget(表示可能有多種狀態)。這裡先不用深究其原理,只需知道這個跟flutter的重新整理等相關。
-
在Widget元件中都是通過build方法來描述自己的內部結構。這裡的build表示構建MyApp中使用的是MaterialApp的系統元件。
-
home標籤的值:Scaffold是Material library 中提供的一個元件,我們可以在裡面設定導航欄、標題和包含主螢幕widget樹的body屬性。可以看到這裡是在頁面上新增了AppBar和一個Text。
-
Center是一個可以把子元件放在中心的元件
開始改造
我們的目標是把頁面中顯示hello_world的TextView換成一個ListView。由上面的分析可知,將上面第4點的home標籤的值,換成一個ListView就能改變頁面顯示的內容。不過在此之前,需要先準備一下要顯示的資料,這裡是使用一個叫 english_words 的三方包,可以幫助我們生成顯示的單詞資料。先學習一下如何新增依賴包:
- 開啟pubspec.yaml檔案新增三方庫:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
english_words: ^3.1.0
複製程式碼
- 點選 Packages get 獲取剛新增的包。
新增english_words庫之後,可以這樣使用這個庫創造資料:
//創造5個隨機片語,並返回片語的迭代器
generateWordPairs().take(5)
複製程式碼
學習使用可變狀態的元件 StatefulWidget
檢視ListView的原始碼,發現其最終是繼承自 StatelessWidget,所以它的狀態是唯一的。但是要實現的ListView中的資料是動態變化的,所以需要使用StatefulWidget來動態改變ListView中的資料。
使用StatefulWidget元件需要自己控制在不同情況下的顯示狀態,所以需要實現State類來告訴StatefulWidget類不同情況下如何展示。 |
建立一個動態變化的元件類,用於表示要顯示的ListView:
class RandomWords extends StatefulWidget {
@override
State<StatefulWidget> createState() { //分析1
return new RandomWordsState();
}
}
複製程式碼
分析:
- 建立State類的方法,這裡繼承系統的State創造一個名叫RandomWordsState的類,來控制ListView各個狀態下的展示。
class RandomWordsState extends State<RandomWords> {
}
複製程式碼
要完成展示資料和儲存點選後的資料,這裡分別用陣列和set來儲存(用set儲存點選後的資料是因為set可以去重,你也可以選擇其他的儲存方式)
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[]; //分析 1
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0); //分析 2
}
複製程式碼
分析:
- Dart中加上 _表示私有化
- 表示字型大小的常量
創造資料集和構建ListView
新增如下兩個方法到RandomWordsState類中,表示創造資料集和構建ListView:
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 對於每個建議的單詞對都會呼叫一次itemBuilder,然後將單詞對新增到ListTile行中
// 在偶數行,該函式會為單詞對新增一個ListTile row.
// 在奇數行,該函式會新增一個分割線widget,來分隔相鄰的詞對。
// 注意,在小螢幕上,分割線看起來可能比較吃力。
itemBuilder: (context, i) {
// 在每一列之前,新增一個1畫素高的分隔線widget
if (i.isOdd) return new Divider();
// 語法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整),比如i為:1, 2, 3, 4, 5
// 時,結果為0, 1, 1, 2, 2, 這可以計算出ListView中減去分隔線後的實際單詞對數量
final index = i ~/ 2;
// 如果是建議列表中最後一個單詞對
if (index >= _suggestions.length) {
// ...接著再生成10個單詞對,然後新增到建議列表
_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);
}
});
},
);
}
複製程式碼
程式碼中有詳細的註釋,但是為了方便理解,這裡還是給出一點解釋:
- _buildSuggestions方法就是返回一個ListView
- _buildRow方法就是返回ListView中的一行(ListTile)如何展示
_buildSuggestions方法中:
- 程式碼中的itemBuilder就是如何顯示一行(ListTile)的配置,其返回值是_buildRow方法
_suggestions.addAll(generateWordPairs().take(10));
就是每次新增10個資料到顯示陣列中
_buildRow方法中:
- 設定了一行(ListTile代表一行內容,在iOS和android中叫cell)如何展示
- ListTile設定了title、trailing(右邊的圖示)和點選事件onTap()
- 會根據有沒有被儲存過,決定右邊顯示什麼圖示
- 當點選時,會把沒有點選過的內容儲存到_saved容器中。
新增跳轉邏輯
新增_pushSaved方法表示如何跳轉到新的頁面並展示選中的資料:
main.dart
void _pushSaved() {
Navigator.of(context).push( // 分析 1
new MaterialPageRoute( // 分析 2
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( // 分析 3
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
複製程式碼
分析: 1.使用Navigator.of(context).push的方式來處理跳轉,需要的引數是一個Route 2.建立頁面Route 3.返回一個新的裡面,裡面的body內容是一個ListView,展示的是_saved中讀取出來的資料
最終,所有程式碼整合是如下的樣子:
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',
theme: new ThemeData(
primaryColor: Colors.red,
),
home: RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new RandomWordsState();
}
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<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'),
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),
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_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);
}
});
},
);
}
}
複製程式碼
執行吧,就能看到最上方的效果。
謝謝觀看這篇文章,如果讓您發現了錯誤或者有好的建議,歡迎在下方評論給我留言。