使用 Flutter 构建界面
介绍如何用 Flutter 构建界面
Flutter widget 采用受 React 启发的现代框架构建。核心思想是用 widget 构建 UI。 Widget 根据当前配置和状态描述其视图应有的外观。当 widget 的状态改变时,widget 会重建其描述,框架将其与先前的描述进行 diff,以确定底层渲染树从一种状态过渡到另一种状态所需的最小变更。
Hello world
#
最简 Flutter 应用只需用 widget 调用 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
并将其设为 widget 树的根。本例中 widget 树由两个 widget 组成:Center
及其子节点 Text。框架强制根 widget 覆盖整个屏幕,因此「Hello, world」文字会显示在屏幕中央。此例需指定文字方向;使用 MaterialApp widget 时会自动处理,后文将演示。
编写应用时,你通常会编写继承 StatelessWidget
或 StatefulWidget
的新 widget,取决于 widget 是否管理状态。
widget 的主要工作是实现 build()
函数,用更低层级的 widget 描述自身。框架依次构建这些 widget,直至底层由表示 RenderObject
的 widget 结束,由后者计算并描述 widget 的几何信息。
基础 widget
#Flutter 自带一系列强大的基础 widget,以下是常用的一些:
Text
Text widget 让你在应用中创建一段样式化文本。
Row, Column
这些 flex widget 让你在水平 (Row) 和垂直 (Column) 方向创建灵活布局,其设计基于 Web 的 flexbox 布局模型。
Stack
Stack widget 不按线性方向(水平或垂直)排列,而按绘制顺序将 widget 叠放。可在 Stack 的子节点上使用 Positioned
widget,相对于栈的上、右、下、左边缘定位。Stack 基于 Web 的绝对定位布局模型。
Container
Container widget 用于创建矩形视觉元素,可用 BoxDecoration
装饰背景、边框或阴影,也可设置外边距、内边距和尺寸约束,还可用矩阵在三维空间中变换。
下面是组合这些及其他 widget 的一些简单示例:
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 图标 集。若使用 Materials 库,通常建议包含这一行。
name: my_app
flutter:
uses-material-design: true
许多 Material Design widget 需要放在 MaterialApp
内才能正确显示并继承主题数据。因此请用 MaterialApp 运行应用。
MyAppBar widget 创建一个高度为 56 逻辑像素的 Container,左右内边距各为 8 像素。在容器内,MyAppBar 使用 Row
布局组织子节点。中间的 title widget 标记为 Expanded,表示它会扩展以填满其他子节点未占用的剩余空间。可以有多个 Expanded 子节点,并通过 Expanded 的 flex
参数决定它们占用可用空间的比例。
MyScaffold widget 在垂直列中组织子节点。列顶部放置 MyAppBar 实例,并向应用栏传入用作标题的 Text
widget。将 widget 作为参数传给其他 widget 是一种强大技巧,可创建可在多种场景复用的通用 widget。最后,MyScaffold 用 Expanded
以居中消息填充剩余空间作为 body。
更多信息请参阅 布局。
使用 Material 组件
#
Flutter 提供多种 widget,帮助你构建符合 Material Design 的应用。
Material 应用以 MaterialApp
widget 开头,它在应用根节点构建多种实用 widget,包括 Navigator——管理以字符串标识的 widget 栈,即「路由」。Navigator 让你在应用各界面间平滑过渡。使用 MaterialApp
widget 完全可选,但是良好实践。
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
widget,并改用 material.dart,应用开始更具 Material 风格。例如,应用栏带有阴影,标题文字会自动继承正确样式,还添加了浮动操作按钮。
注意 widget 会作为参数传给其他 widget。
Scaffold
widget 接收多种不同 widget 作为命名参数,各自放在 Scaffold 布局的合适位置。同样,AppBar
widget 让你为 leading、title
的 actions
传入 widget。这一模式在框架中反复出现,设计自己的 widget 时也可考虑采用。
更多信息请参阅 Material 组件 widget。
处理手势
#大多数应用都包含与系统的某种用户交互。构建交互式应用的第一步是检测输入手势。通过创建一个简单按钮来了解其工作原理:
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
widget 没有视觉表现,而是检测用户做出的手势。当用户点击 Container
时,GestureDetector 会调用其 onTap()
回调,本例中向控制台打印消息。你可以用 GestureDetector 检测多种输入手势,包括点击、拖动和缩放。
许多 widget 内部使用 GestureDetector
为其他 widget 提供可选回调。例如,IconButton、ElevatedButton
和 FloatingActionButton
widget 具有 onPressed()
回调,在用户点击 widget 时触发。
更多信息请参阅 Flutter 中的手势。
根据输入更改 widget
#
到目前为止,本页只使用了无状态 widget。无状态 widget 从父 widget 接收参数,并存入 final
成员变量。当要求 widget build()
时,它用这些存储的值为其创建的 widget 推导新参数。
要构建更复杂的体验——例如以更有趣的方式响应用户输入——应用通常需要持有一些状态。
Flutter 用 StatefulWidget 表达这一概念。
StatefulWidget 是知道如何生成 State 对象的特殊 widget,再由 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 中,这两类对象生命周期不同。
Widget 是临时对象,用于构建应用在某一状态下的呈现。
State 对象则在多次调用 build() 之间保持存在,从而能记住信息。
上面的示例接受用户输入并直接在 build() 方法中使用结果。在更复杂的应用中,widget 树的不同部分可能负责不同关注点;例如,一个 widget 可能展示用于收集日期或位置等特定信息的复杂界面,另一个 widget 则可能用这些信息改变整体呈现。
在 Flutter 中,变更通知通过回调沿 widget 层次结构向上流动,当前状态则向下流向负责呈现的无状态 widget。重定向这一流动的共同父级是 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())),
),
);
}
注意这里创建了两个新的无状态 widget,清晰分离了 显示 计数器 (CounterDisplay) 与 修改 计数器 (CounterIncrementor) 的职责。尽管总体结果与前一示例相同,职责分离使各 widget 能封装更复杂的逻辑,同时保持父 widget 简洁。
更多信息请参阅:
综合示例
#
下面是一个更完整的示例,综合上述概念:假设某购物应用展示待售商品,并维护意向购买的购物车。先从定义呈现类 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 widget 遵循无状态 widget 的常见模式:将构造函数接收的值存入 final
成员变量,并在 build()
函数中使用。例如,inCart 布尔值在两种视觉外观间切换:一种使用当前主题的主色,另一种使用灰色。
当用户点击列表项时,widget 不会直接修改 inCart 值,而是调用从父 widget 收到的 onCartChanged 函数。这一模式让你能把状态保存在 widget 层次结构更高处,使状态持续更久。极端情况下,传给 runApp()
的 widget 上保存的状态会贯穿整个应用生命周期。
当父级收到 onCartChanged 回调时,会更新内部状态,从而触发父级重建并创建带有新 inCart 值的 ShoppingListItem
新实例。尽管父级重建时会创建新的 ShoppingListItem 实例,但这一操作开销很小,因为框架会将新构建的 widget 与先前构建的 widget 比较,并仅将差异应用到底层 RenderObject。
下面是一个保存可变状态的父 widget 示例:
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,表示该 widget 保存可变状态。当 ShoppingList widget 首次插入树时,框架调用 createState()
创建新的 _ShoppingListState 实例并与树中该位置关联。(注意 State
的子类通常以下划线开头,表示它们是私有实现细节。)当该 widget 的父级重建时,父级会创建新的 ShoppingList 实例,但框架会复用树中已有的 _ShoppingListState 实例,而不会再次调用 createState。
要访问当前 ShoppingList 的属性,_ShoppingListState 可使用其 widget
属性。若父级重建并创建新的 ShoppingList,_ShoppingListState 会用新的 widget 值重建。若希望在 widget 属性变化时收到通知,可重写 didUpdateWidget()
函数,它会传入 oldWidget 以便你将旧 widget 与当前 widget 比较。
处理 onCartChanged 回调时,
_ShoppingListState 通过向 _shoppingCart 添加或移除商品来变更内部状态。为向框架表明内部状态已改变,这些调用应包在 setState()
中。调用 setState 会将该 widget 标记为 dirty,并在应用下次需要更新屏幕时安排重建。若在修改 widget 内部状态时忘记调用 setState,框架不会知道 widget 已 dirty,可能不会调用 widget 的 build()
函数,界面也就可能不会反映变更后的状态。用这种方式管理状态时,你无需为创建和更新子 widget 分别编写代码,只需实现 build 函数,它可同时处理两种情况。
响应 widget 生命周期事件
#
在 StatefulWidget 上调用 createState()
之后,框架将新的 state 对象插入树,然后在该 state 对象上调用 initState()。
State
的子类可重写 initState 以执行只需进行一次的工作,例如配置动画或订阅平台服务。initState 的实现必须先调用 super.initState。
当不再需要 state 对象时,框架会在该 state 对象上调用 dispose()。可重写 dispose 函数进行清理,例如取消定时器或取消订阅平台服务。
dispose 的实现通常以调用 super.dispose 结束。
更多信息请参阅 State。
Key
#
使用 key 可控制 widget 重建时框架将哪些 widget 相互匹配。默认情况下,框架根据 runtimeType
及出现顺序匹配当前构建与先前构建中的 widget。有了 key,框架还要求两个 widget 具有相同的 key
以及相同的 runtimeType。
key 在构建大量同类型 widget 实例时最有用。例如 ShoppingList widget 会构建刚好填满可见区域的 ShoppingListItem 实例:
-
没有 key 时,当前构建中的第一项总会与先前构建中的第一项同步,即使从语义上讲列表第一项已滚出屏幕、在视口中不再可见。
-
为列表中每项分配「语义」key 后,无限列表可以更高效,因为框架会同步具有匹配语义 key 的项,从而保持相似(或相同)的视觉外观。此外,按语义同步项意味着有状态子 widget 中保留的状态会附着在相同语义项上,而不是视口中相同数值位置的项上。
更多信息请参阅 Key
API。
Global keys
#Global key
#使用 global key 可唯一标识子 widget。 global key 必须在整个 widget 层次结构中全局唯一,而 local key 只需在兄弟节点间唯一。由于全局唯一,global key 可用于获取与 widget 关联的 state。
更多信息请参阅 GlobalKey
API。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-17。查看文档源码 或者 为本页面内容提出建议。