Flutter 是一个流行的开源工具包,它可用于构建跨平台的应用。在文章《用 Flutter 创建移动应用》中,我已经向大家展示了如何在 Linux 中安装 Flutter 并创建你的第一个应用。而这篇文章,我将向你展示如何在你的应用中添加一个列表,点击每一个列表项可以打开一个新的界面。这是移动应用的一种常见设计方法,你可能以前见过的,下面有一个截图,能帮助你对它有一个更直观的了解:
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 文件:
将新建的文件命名为 items_list_page
。
切换回到 main.dart
文件,将 MyHomePage
和 _MyHomePageState
中的代码,剪切并粘贴到我们新建的文件。然后将光标放到 StatefulWidget
上(下面红色的下划线处), 按 Alt Enter
后出现下拉列表,然后选择 package:flutter/material.dart
:
经过上面的操作我们将 flutter/material.dart
包添加到了 main.dart
文件中,这样我们就可以使用 Flutter 提供的默认的 material 主题微件。
然后, 在类名 MyHomePage
右击,“Refactor > Rename...”将其重命名为 ItemsListPage
:
Flutter 识别到你重命名了 StatefulWidget
类,它会自动将它的 State
类也跟着重命名:
回到 main.dart
文件,将文件名 MyHomePage
改为 ItemsListPage
。 一旦你开始输入, 你的 Flutter 集成开发环境(可能是 IntelliJ IDEA 社区版、Android Studio 和 VS Code 或 VSCodium),会给出自动代码补完的建议。
到此,你已经完成了初始设置。现在你需要在 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...”
输入 Hero
,然后从建议的下拉列表中选择 Hero((Key key, @required this, tag, this.create))
:
最后我们在 item_details_page.dart
文件中做相同的修改:
前面的步骤,其实我们是用 Hero()
微件对 Icon()
微件进行了封装。还记得吗?前面我们定义 ItemModel
类时,定义了一个 id field
,但没有在任何地方使用到。因为 Hero 微件会为其每个子微件添加一个唯一的标签。当 Hero 检测到不同页面(MaterialPageRoute
)中存在相同标签的 Hero 时,它会自动在这些不同的页面中应用过渡动画。
可以在安卓模拟器或物理设备上运行我们的应用来测试这个动画。当你打开或者关闭列表项的详情页时,你会看到一个漂亮的图标动画: