使用 Flutter 构建用户界面
Flutter 组件采用受 React 启发的现代框架构建。其核心思想是,你使用组件构建用户界面。组件描述了在给定当前配置和状态的情况下,其视图应该是什么样子。当组件的状态发生变化时,组件会重建其描述,框架会将该描述与之前的描述进行比较,以确定底层渲染树中从一个状态转换到下一个状态所需的最小更改。
Hello world
#最简单的 Flutter 应用只需使用一个组件调用 runApp()
函数
import 'package:flutter/material.dart';
void main() {
runApp(
const Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.blue),
),
),
);
}
runApp()
函数接收给定的 Widget
并使其成为组件树的根。在此示例中,组件树由两个组件组成:Center
组件及其子组件 Text
组件。框架强制根组件覆盖屏幕,这意味着“Hello, world”文本最终会居中显示。在此实例中需要指定文本方向;当使用 MaterialApp
组件时,这会自动为你处理,如下面所示。
在编写应用时,你通常会根据组件是否管理任何状态,编写继承自 StatelessWidget
或 StatefulWidget
的新组件。组件的主要任务是实现一个 build()
函数,该函数使用其他低级组件来描述该组件。框架会依次构建这些组件,直到最终构建代表底层 RenderObject
的组件,RenderObject
计算并描述组件的几何形状。
基本组件
#Flutter 带有一套功能强大的基本组件,其中以下组件是常用的
文本
Text
组件允许你在应用中创建一段带样式的文本。Row
,Column
- 这些弹性组件允许你在水平 (
Row
) 和垂直 (Column
) 方向上创建灵活的布局。这些对象的设计基于 Web 的 flexbox 布局模型。 层叠布局
Stack
组件不是线性定向(水平或垂直)的,它允许你按绘制顺序将组件相互堆叠。然后,你可以使用Positioned
组件对Stack
的子组件进行定位,使其相对于堆栈的顶部、右侧、底部或左侧边缘。堆栈基于 Web 的绝对定位布局模型。容器
Container
组件允许你创建一个矩形的可视元素。容器可以使用BoxDecoration
进行装饰,例如背景、边框或阴影。Container
还可以应用边距、填充和大小约束。此外,Container
可以使用矩阵在三维空间中进行变换。
下面是一些结合了这些组件和其他组件的简单组件
import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget {
const MyAppBar({required this.title, super.key});
// Fields in a Widget subclass are always marked "final".
final Widget title;
@override
Widget build(BuildContext context) {
return Container(
height: 56, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: Row(
children: [
const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child
// to fill the available space.
Expanded(child: title),
const IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
);
}
}
class MyScaffold extends StatelessWidget {
const MyScaffold({super.key});
@override
Widget build(BuildContext context) {
// Material is a conceptual piece
// of paper on which the UI appears.
return Material(
// Column is a vertical, linear layout.
child: Column(
children: [
MyAppBar(
title: Text(
'Example title',
style:
Theme.of(context) //
.primaryTextTheme
.titleLarge,
),
),
const Expanded(child: Center(child: Text('Hello, world!'))),
],
),
);
}
}
void main() {
runApp(
const MaterialApp(
title: 'My app', // used by the OS task switcher
home: SafeArea(child: MyScaffold()),
),
);
}
确保在 pubspec.yaml
文件的 flutter
部分有一个 uses-material-design: true
条目。它允许你使用预定义的一组 Material 图标。如果你正在使用 Material 库,通常最好包含此行。
name: my_app
flutter:
uses-material-design: true
许多 Material Design 组件需要放置在 MaterialApp
内才能正确显示,以便继承主题数据。因此,请使用 MaterialApp
运行应用程序。
MyAppBar
组件创建一个 Container
,其高度为 56 设备独立像素,左右内边距均为 8 像素。在容器内部,MyAppBar
使用 Row
布局来组织其子组件。中间的子组件,即 title
组件,被标记为 Expanded
,这意味着它会扩展以填充未被其他子组件占用的任何剩余可用空间。你可以有多个 Expanded
子组件,并使用 Expanded
的 flex
参数确定它们占用可用空间的比例。
MyScaffold
组件将其子组件组织成垂直列。在列的顶部,它放置了一个 MyAppBar
实例,并向应用栏传递一个 Text
组件作为其标题。将组件作为参数传递给其他组件是一种强大的技术,它允许你创建可以在多种方式中重用的通用组件。最后,MyScaffold
使用 Expanded
来用其主体填充剩余空间,主体包含一个居中的消息。
欲了解更多信息,请查看 布局。
使用 Material Components
#Flutter 提供了许多组件来帮助你构建遵循 Material Design 的应用。Material 应用以 MaterialApp
组件开头,该组件在应用的根目录构建了许多有用的组件,包括 Navigator
,它管理着一堆由字符串标识的组件,也称为“路由”。Navigator
允许你在应用程序的不同屏幕之间平滑过渡。使用 MaterialApp
组件是完全可选的,但这是一个好的实践。
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(title: 'Flutter Tutorial', home: TutorialHome()));
}
class TutorialHome extends StatelessWidget {
const TutorialHome({super.key});
@override
Widget build(BuildContext context) {
// Scaffold is a layout for
// the major Material Components.
return Scaffold(
appBar: AppBar(
leading: const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: const Text('Example title'),
actions: const [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body is the majority of the screen.
body: const Center(child: Text('Hello, world!')),
floatingActionButton: const FloatingActionButton(
tooltip: 'Add', // used by assistive technologies
onPressed: null,
child: Icon(Icons.add),
),
);
}
}
现在代码已经从 MyAppBar
和 MyScaffold
切换到 AppBar
和 Scaffold
组件接受多个不同的组件作为命名参数,每个组件都放置在 Scaffold
布局中的适当位置。类似地,AppBar
组件允许你为 leading
组件以及 title
组件的 actions
传递组件。这种模式在整个框架中反复出现,并且在你设计自己的组件时也应考虑。
欲了解更多信息,请查看 Material Components 组件。
处理手势
#大多数应用程序都包含某种形式的用户与系统的交互。构建交互式应用程序的第一步是检测输入手势。通过创建一个简单的按钮来了解其工作原理
import 'package:flutter/material.dart';
class MyButton extends StatelessWidget {
const MyButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print('MyButton was tapped!');
},
child: Container(
height: 50,
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.lightGreen[500],
),
child: const Center(child: Text('Engage')),
),
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: MyButton())),
),
);
}
GestureDetector
组件没有可视表示,而是检测用户做出的手势。当用户点击 Container
时,GestureDetector
会调用其 onTap()
回调,在此示例中是向控制台打印消息。你可以使用 GestureDetector
来检测各种输入手势,包括点击、拖动和缩放。
许多组件使用 GestureDetector
为其他组件提供可选的回调。例如,IconButton
、ElevatedButton
和 FloatingActionButton
组件都有 onPressed()
回调,当用户点击组件时会触发这些回调。
欲了解更多信息,请查看 Flutter 中的手势。
根据输入更改组件
#到目前为止,本页只使用了无状态组件。无状态组件从其父组件接收参数,并将其存储在 final
成员变量中。当组件被要求 build()
时,它会使用这些存储的值为其创建的组件派生新参数。
为了构建更复杂的体验,例如以更有趣的方式响应用户输入,应用程序通常会携带一些状态。Flutter 使用 StatefulWidgets
来捕捉这一思想。StatefulWidgets
是特殊的组件,它们知道如何生成 State
对象,然后这些对象用于保存状态。考虑下面这个基本示例,使用了前面提到的 ElevatedButton
import 'package:flutter/material.dart';
class Counter extends StatefulWidget {
// This class is the configuration for the state.
// It holds the values (in this case nothing) provided
// by the parent and used by the build method of the
// State. Fields in a Widget subclass are always marked
// "final".
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
// This call to setState tells the Flutter framework
// that something has changed in this State, which
// causes it to rerun the build method below so that
// the display can reflect the updated values. If you
// change _counter without calling setState(), then
// the build method won't be called again, and so
// nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called,
// for instance, as done by the _increment method above.
// The Flutter framework has been optimized to make
// rerunning build methods fast, so that you can just
// rebuild anything that needs updating rather than
// having to individually changes instances of widgets.
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(onPressed: _increment, child: const Text('Increment')),
const SizedBox(width: 16),
Text('Count: $_counter'),
],
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: Counter())),
),
);
}
你可能会疑惑为什么 StatefulWidget
和 State
是独立的两个对象。在 Flutter 中,这两种对象具有不同的生命周期。Widgets
是临时对象,用于构建应用程序当前状态的表示。另一方面,State
对象在对 build()
的调用之间是持久的,允许它们记住信息。
上面的示例接受用户输入并直接在其 build()
方法中使用结果。在更复杂的应用程序中,组件层次结构的不同部分可能负责不同的关注点;例如,一个组件可能呈现一个复杂的用户界面,旨在收集特定信息,例如日期或位置,而另一个组件可能使用该信息来改变整体呈现。
在 Flutter 中,更改通知通过回调“向上”流经组件层次结构,而当前状态“向下”流向进行演示的无状态组件。重定向此流动的共同父级是 State
。以下这个稍微复杂的示例展示了这在实践中是如何工作的
import 'package:flutter/material.dart';
class CounterDisplay extends StatelessWidget {
const CounterDisplay({required this.count, super.key});
final int count;
@override
Widget build(BuildContext context) {
return Text('Count: $count');
}
}
class CounterIncrementor extends StatelessWidget {
const CounterIncrementor({required this.onPressed, super.key});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: onPressed, child: const Text('Increment'));
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CounterIncrementor(onPressed: _increment),
const SizedBox(width: 16),
CounterDisplay(count: _counter),
],
);
}
}
void main() {
runApp(
const MaterialApp(
home: Scaffold(body: Center(child: Counter())),
),
);
}
请注意创建了两个新的无状态组件,清楚地分离了**显示**计数器 (CounterDisplay
) 和**更改**计数器 (CounterIncrementor
) 的关注点。尽管最终结果与前一个示例相同,但职责分离允许在单个组件中封装更大的复杂性,同时保持父组件的简洁性。
欲了解更多信息,请查看
融会贯通
#接下来是一个更完整的示例,它将这些概念结合在一起:一个假想的购物应用程序显示各种待售商品,并维护一个用于预期购买的购物车。首先定义表示类 ShoppingListItem
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = void Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different
// parts of the tree can have different themes.
// The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}
TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;
return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ShoppingListItem(
product: const Product(name: 'Chips'),
inCart: true,
onCartChanged: (product, inCart) {},
),
),
),
),
);
}
ShoppingListItem
组件遵循无状态组件的常见模式。它将其在构造函数中接收到的值存储在 final
成员变量中,然后在 build()
函数中使用这些值。例如,inCart
布尔值在两种视觉外观之间切换:一种使用当前主题的主色,另一种使用灰色。
当用户点击列表项时,组件不会直接修改其 inCart
值。相反,组件调用它从其父组件接收到的 onCartChanged
函数。这种模式允许你将状态存储在组件层次结构中较高的位置,从而使状态持续更长的时间。在极端情况下,存储在传递给 runApp()
的组件上的状态在应用程序的整个生命周期中都存在。
当父组件收到 onCartChanged
回调时,父组件会更新其内部状态,这会触发父组件重建并创建具有新 inCart
值的新 ShoppingListItem
实例。尽管父组件在重建时会创建一个新的 ShoppingListItem
实例,但该操作成本很低,因为框架会将新构建的组件与之前构建的组件进行比较,并且只将差异应用于底层 RenderObject
。
这是一个存储可变状态的父组件示例
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = void Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different
// parts of the tree can have different themes.
// The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}
TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;
return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.key});
final List<Product> products;
// The framework calls createState the first time
// a widget appears at a given location in the tree.
// If the parent rebuilds and uses the same type of
// widget (with the same key), the framework re-uses
// the State object instead of creating a new State object.
@override
State<ShoppingList> createState() => _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When a user changes what's in the cart, you need
// to change _shoppingCart inside a setState call to
// trigger a rebuild.
// The framework then calls build, below,
// which updates the visual appearance of the app.
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
void main() {
runApp(
const MaterialApp(
title: 'Shopping App',
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
),
);
}
ShoppingList
类扩展了 StatefulWidget
,这意味着此组件存储可变状态。当 ShoppingList
组件首次插入到树中时,框架会调用 createState()
函数以创建 _ShoppingListState
的新实例,并将其与树中的该位置关联。(请注意,State
的子类通常以开头的下划线命名,表示它们是私有实现细节。)当此组件的父组件重建时,父组件会创建一个新的 ShoppingList
实例,但框架会重用树中已有的 _ShoppingListState
实例,而不是再次调用 createState
。
要访问当前 ShoppingList
的属性,_ShoppingListState
可以使用其 widget
属性。如果父组件重建并创建新的 ShoppingList
,_ShoppingListState
将使用新的组件值进行重建。如果你希望在 widget
属性更改时收到通知,请重写 didUpdateWidget()
函数,该函数会传入一个 oldWidget
,以便你可以比较旧组件与当前组件。
在处理 onCartChanged
回调时,_ShoppingListState
通过向 _shoppingCart
添加或移除产品来修改其内部状态。为了向框架表明其内部状态已更改,它将这些调用包装在 setState()
调用中。调用 setState
会将此组件标记为脏,并安排它在应用程序下次需要更新屏幕时重建。如果你在修改组件内部状态时忘记调用 setState
,框架将不知道你的组件已脏,并且可能不会调用组件的 build()
函数,这意味着用户界面可能不会更新以反映更改的状态。通过这种方式管理状态,你无需为创建和更新子组件编写单独的代码。相反,你只需实现 build
函数,它会处理这两种情况。
响应组件生命周期事件
#在 StatefulWidget
上调用 createState()
后,框架会将新的状态对象插入树中,然后调用状态对象的 initState()
。State
的子类可以重写 initState
来执行只需要发生一次的工作。例如,重写 initState
以配置动画或订阅平台服务。initState
的实现必须先调用 super.initState
。
当不再需要状态对象时,框架会调用状态对象的 dispose()
。重写 dispose
函数来执行清理工作。例如,重写 dispose
以取消计时器或取消订阅平台服务。dispose
的实现通常以调用 super.dispose
结束。
欲了解更多信息,请查看 State
。
键(Keys)
#当组件重建时,使用键来控制框架将哪些组件与其他组件匹配。默认情况下,框架根据它们的 runtimeType
以及它们出现的顺序来匹配当前和之前构建中的组件。使用键,框架要求两个组件具有相同的 key
以及相同的 runtimeType
。
键在构建许多相同类型组件实例的组件中最有用。例如,ShoppingList
组件,它只构建足够的 ShoppingListItem
实例来填充其可见区域
如果没有键,当前构建中的第一个条目将始终与之前构建中的第一个条目同步,即使从语义上讲,列表中的第一个条目只是滚出屏幕并且在视口中不再可见。
通过为列表中的每个条目分配一个“语义”键,无限列表可以更高效,因为框架会同步具有匹配语义键的条目,因此具有相似(或相同)的视觉外观。此外,语义同步条目意味着有状态子组件中保留的状态仍然附加到相同的语义条目,而不是视口中相同数字位置的条目。
欲了解更多信息,请查看 Key
API。
全局键(Global keys)
#使用全局键(global keys)来唯一标识子组件。全局键必须在整个组件层次结构中全局唯一,与局部键不同,局部键只需在兄弟组件之间唯一。由于它们是全局唯一的,因此可以使用全局键来检索与组件关联的状态。
欲了解更多信息,请查看 GlobalKey
API。