在 Flutter 移動應用程序中創建一個列表
Flutter 是一個流行的開源工具包,它可用於構建跨平台的應用。在文章《用 Flutter 創建移動應用》中,我已經向大家展示了如何在 Linux 中安裝 Flutter 並創建你的第一個應用。而這篇文章,我將向你展示如何在你的應用中添加一個列表,點擊每一個列表項可以打開一個新的界面。這是移動應用的一種常見設計方法,你可能以前見過的,下面有一個截圖,能幫助你對它有一個更直觀的了解:
![測試 Flutter 應用](/data/attachment/album/202105/31/201524m9453rl9mtmdzx99.gif "Testing the Flutter app")
Flutter 使用 Dart 語言。在下面的一些代碼片段中,你會看到以斜杠開頭的語句。兩個斜杠(//
)是指代碼注釋,用於解釋某些代碼片段。三個斜杠(///
)則表示的是 Dart 的文檔注釋,用於解釋 Dart 類和類的屬性,以及其他的一些有用的信息。
查看Flutter應用的主要部分
Flutter 應用的典型入口點是 main()
函數,我們通常可以在文件 lib/main.dart
中找到它:
void main() {
runApp(MyApp());
}
應用啟動時,main()
會被調用,然後執行 MyApp()
。 MyApp
是一個無狀態微件(StatelessWidget
),它包含了MaterialApp()
微件中所有必要的應用設置(應用的主題、要打開的初始頁面等):
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
生成的 MyHomePage()
是應用的初始頁面,是一個有狀態的微件,它包含包含可以傳遞給微件構造函數參數的變數(從上面的代碼看,我們傳了一個 title
變數給初始頁面的構造函數):
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
有狀態微件(StatefulWidget
)表示這個微件可以擁有自己的狀態:_MyHomePageState
。調用 _MyHomePageState
中的 setState()
方法,可以重新構建用戶界面:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; // Number of taps on + button.
void _incrementCounter() { // Increase number of taps and update UI by calling setState().
setState(() {
_counter++;
});
}
...
}
不管是有狀態的,還是無狀態的微件,它們都有一個 build()
方法,該方法負責微件的 UI 外觀。
@override
Widget build(BuildContext context) {
return Scaffold( // Page widget.
appBar: AppBar( // Page app bar with title and back button if user can return to previous screen.
title: Text(widget.title), // Text to display page title.
),
body: Center( // Widget to center child widget.
child: Column( // Display children widgets in column.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text( // Static text.
'You have pushed the button this many times:',
),
Text( // Text with our taps number.
'$_counter', // $ sign allows us to use variables inside a string.
style: Theme.of(context).textTheme.headline4,// Style of the text, 「Theme.of(context)」 takes our context and allows us to access our global app theme.
),
],
),
),
// Floating action button to increment _counter number.
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
修改你的應用
一個好的做法是,把 main()
方法和其他頁面的代碼分開放到不同的文件中。要想將它們分開,你需要右擊 lib
目錄,然後選擇 「New > Dart File」 來創建一個 .dart 文件:
![創建一個新的 Dart 文件](/data/attachment/album/202105/31/201524vap110sxfx80p21z.png "Create a new Dart file")
將新建的文件命名為 items_list_page
。
切換回到 main.dart
文件,將 MyHomePage
和 _MyHomePageState
中的代碼,剪切並粘貼到我們新建的文件。然後將游標放到 StatefulWidget
上(下面紅色的下劃線處), 按 Alt+Enter
後出現下拉列表,然後選擇 package:flutter/material.dart
:
![導入 Flutter 包](/data/attachment/album/202105/31/201524zlsefqh50efa8g5a.png "Importing Flutter package")
經過上面的操作我們將 flutter/material.dart
包添加到了 main.dart
文件中,這樣我們就可以使用 Flutter 提供的默認的 material 主題微件。
然後, 在類名 MyHomePage
右擊,「Refactor > Rename...」將其重命名為 ItemsListPage
:
![重命名 StatefulWidget 類](/data/attachment/album/202105/31/201525tzuhx5xtxk4tp99s.png "Renaming StatefulWidget class")
Flutter 識別到你重命名了 StatefulWidget
類,它會自動將它的 State
類也跟著重命名:
![State 類被自動重命名](/data/attachment/album/202105/31/201525d6w2q2kiiijzni0w.png "State class renamed automatically")
回到 main.dart
文件,將文件名 MyHomePage
改為 ItemsListPage
。 一旦你開始輸入, 你的 Flutter 集成開發環境(可能是 IntelliJ IDEA 社區版、Android Studio 和 VS Code 或 VSCodium),會給出自動代碼補完的建議。
![IDE 建議自動補完的代碼](/data/attachment/album/202105/31/201525xbzpb6bwwf595clw.png "IDE suggests autocompleting code")
按回車鍵即可完成輸入,缺失的導入語句會被自動添加到文件的頂部。
![添加缺失的導入語句](/data/attachment/album/202105/31/201526erzso7rg6g0sdsqk.png "Adding missing import ")
到此,你已經完成了初始設置。現在你需要在 lib
目錄創建一個新的 .dart 文件,命名為 item_model
。(注意,類命是大寫駝峰命名,一般的文件名是下劃線分割的命名。)然後粘貼下面的代碼到新的文件中:
/// Class that stores list item info:
/// [id] - unique identifier, number.
/// [icon] - icon to display in UI.
/// [title] - text title of the item.
/// [description] - text description of the item.
class ItemModel {
// class constructor
ItemModel(this.id, this.icon, this.title, this.description);
// class fields
final int id;
final IconData icon;
final String title;
final String description;
}
回到 items_list_page.dart
文件,將已有的 _ItemsListPageState
代碼替換為下面的代碼:
class _ItemsListPageState extends State<ItemsListPage> {
// Hard-coded list of [ItemModel] to be displayed on our page.
final List<ItemModel> _items = [
ItemModel(0, Icons.account_balance, 'Balance', 'Some info'),
ItemModel(1, Icons.account_balance_wallet, 'Balance wallet', 'Some info'),
ItemModel(2, Icons.alarm, 'Alarm', 'Some info'),
ItemModel(3, Icons.my_location, 'My location', 'Some info'),
ItemModel(4, Icons.laptop, 'Laptop', 'Some info'),
ItemModel(5, Icons.backup, 'Backup', 'Some info'),
ItemModel(6, Icons.settings, 'Settings', 'Some info'),
ItemModel(7, Icons.call, 'Call', 'Some info'),
ItemModel(8, Icons.restore, 'Restore', 'Some info'),
ItemModel(9, Icons.camera_alt, 'Camera', 'Some info'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder( // Widget which creates [ItemWidget] in scrollable list.
itemCount: _items.length, // Number of widget to be created.
itemBuilder: (context, itemIndex) => // Builder function for every item with index.
ItemWidget(_items[itemIndex], () {
_onItemTap(context, itemIndex);
}),
));
}
// Method which uses BuildContext to push (open) new MaterialPageRoute (representation of the screen in Flutter navigation model) with ItemDetailsPage (StateFullWidget with UI for page) in builder.
_onItemTap(BuildContext context, int itemIndex) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ItemDetailsPage(_items[itemIndex])));
}
}
// StatelessWidget with UI for our ItemModel-s in ListView.
class ItemWidget extends StatelessWidget {
const ItemWidget(this.model, this.onItemTap, {Key key}) : super(key: key);
final ItemModel model;
final Function onItemTap;
@override
Widget build(BuildContext context) {
return InkWell( // Enables taps for child and add ripple effect when child widget is long pressed.
onTap: onItemTap,
child: ListTile( // Useful standard widget for displaying something in ListView.
leading: Icon(model.icon),
title: Text(model.title),
),
);
}
}
為了提高代碼的可讀性,可以考慮將 ItemWidget
作為一個單獨的文件放到 lib
目錄中。
現在唯一缺少的是 ItemDetailsPage
類。在 lib
目錄中我們創建一個新文件並命名為 item_details_page
。然後將下面的代碼拷貝進去:
import 'package:flutter/material.dart';
import 'item_model.dart';
/// Widget for displaying detailed info of [ItemModel]
class ItemDetailsPage extends StatefulWidget {
final ItemModel model;
const ItemDetailsPage(this.model, {Key key}) : super(key: key);
@override
_ItemDetailsPageState createState() => _ItemDetailsPageState();
}
class _ItemDetailsPageState extends State<ItemDetailsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.model.title),
),
body: Center(
child: Column(
children: [
const SizedBox(height: 16),
Icon(
widget.model.icon,
size: 100,
),
const SizedBox(height: 16),
Text(
'Item description: ${widget.model.description}',
style: TextStyle(fontSize: 18),
)
],
),
),
);
}
}
上面的代碼幾乎沒什麼新東西,不過要注意的是 _ItemDetailsPageState
里使用了 widget.item.title
這樣的語句,它讓我們可以從有狀態類中引用到其對應的微件(StatefulWidget
)。
添加一些動畫
現在讓我們來添加一些基礎的動畫:
- 找到
ItemWidget
代碼塊(或者文件) - 將游標放到
build()
方法中的Icon()
微件上 - 按
Alt+Enter
,然後選擇「Wrap with widget...」
![查看微件選項](/data/attachment/album/202105/31/201526rrcimnffoz2bfz05.png "Wrap with widget option")
輸入 Hero
,然後從建議的下拉列表中選擇 Hero((Key key, @required this, tag, this.create))
:
![查找 Hero 微件](/data/attachment/album/202105/31/201526c33izlcl6gklkyet.png "Finding the Hero widget")
下一步, 給 Hero 微件添加 tag
屬性 tag: model.id
:
![在 Hero 微件上添加 tag 屬性為 model.id](/data/attachment/album/202105/31/201527u3ifl1y4uxu1sevv.png "Adding the tag property model.id to the Hero widget")
最後我們在 item_details_page.dart
文件中做相同的修改:
![修改item_details_page.dart文件](/data/attachment/album/202105/31/201527t90kkp90mm6op9wx.png "Changing item_details_page.dart file")
前面的步驟,其實我們是用 Hero()
微件對 Icon()
微件進行了封裝。還記得嗎?前面我們定義 ItemModel
類時,定義了一個 id field
,但沒有在任何地方使用到。因為 Hero 微件會為其每個子微件添加一個唯一的標籤。當 Hero 檢測到不同頁面(MaterialPageRoute
)中存在相同標籤的 Hero 時,它會自動在這些不同的頁面中應用過渡動畫。
可以在安卓模擬器或物理設備上運行我們的應用來測試這個動畫。當你打開或者關閉列表項的詳情頁時,你會看到一個漂亮的圖標動畫:
![測試 Flutter 應用](/data/attachment/album/202105/31/201524m9453rl9mtmdzx99.gif "Testing the Flutter app")
收尾
這篇教程,讓你學到了:
- 一些符合標準的,且能用於自動創建應用的組件。
- 如何添加多個頁面以及在頁面間傳遞數據。
- 如何給多個頁面添加簡單的動畫。
如果你想了解更多,查看 Flutter 的 文檔(有一些視頻和樣例項目的鏈接,還有一些創建 Flutter 應用的「秘方」)與 源碼,源碼的開源許可證是 BSD 3。
via: https://opensource.com/article/20/11/flutter-lists-mobile-app
作者:Vitaly Kuprenko 選題:lujun9972 譯者:ywxgod 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive