使用 Flutter 构建用户界面
Flutter 组件基于一个借鉴了 React 的现代框架构建。核心思想是,你使用组件来构建 UI。组件描述了它们在当前配置和状态下应有的视图外观。当组件的状态发生变化时,组件会重新构建其描述,框架会将新描述与之前的描述进行比较,以确定底层渲染树需要进行的最少更改,从而实现从一种状态到下一种状态的转换。
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
的组件,该对象计算并描述组件的几何形状。
基本组件
#Flutter 提供了一套强大的基本组件,其中以下组件常用:
文本
Text
组件允许你在应用程序中创建一系列带样式的文本。Row
,Column
- 这些弹性组件允许你在水平 (
Row
) 和垂直 (Column
) 方向上创建灵活的布局。这些对象的设计基于 Web 的 Flexbox 布局模型。 层叠布局
- 与线性布局(水平或垂直)不同,
Stack
组件允许你以绘制顺序将组件彼此堆叠。然后,你可以在Stack
的子组件上使用Positioned
组件,以相对于堆栈的顶部、右侧、底部或左侧边缘定位它们。层叠布局基于 Web 的绝对定位布局模型。 容器
Container
组件允许你创建一个矩形的可视元素。容器可以通过BoxDecoration
进行装饰,例如背景、边框或阴影。Container
还可以具有外边距 (margin)、内边距 (padding) 和应用于其大小的约束。此外,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
组件创建一个高度为 56 设备独立像素,左右内边距均为 8 像素的 Container
。在容器内部,MyAppBar
使用 Row
布局来组织其子组件。中间的子组件,即 title
组件,被标记为 Expanded
,这意味着它会扩展以填充其他子组件未占用的所有剩余可用空间。你可以拥有多个 Expanded
子组件,并使用 Expanded
的 flex
参数来确定它们占用可用空间的比例。
MyScaffold
组件将其子组件组织成垂直列。在列的顶部,它放置了一个 MyAppBar
实例,并向应用栏传递一个 Text
组件作为其标题。将组件作为参数传递给其他组件是一种强大的技术,它允许你创建可以在多种方式中复用的通用组件。最后,MyScaffold
使用 Expanded
来用其主体(一个居中的消息)填充剩余空间。
更多信息,请查阅布局。
使用 Material 组件
#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
组件,并从 material.dart
导入,应用开始看起来更像 Material Design 风格了。例如,应用栏带有阴影,标题文本自动继承了正确的样式。还添加了一个浮动操作按钮。
请注意,组件是作为参数传递给其他组件的。Scaffold
组件接受多个不同的组件作为命名参数,每个参数都放置在 Scaffold
布局中的适当位置。类似地,AppBar
组件允许你为 leading
组件、actions
以及 title
组件传入组件。这种模式在整个框架中反复出现,也是你在设计自己的组件时可能需要考虑的。
更多信息,请查阅Material 组件。
处理手势
#大多数应用程序都包含某种形式的用户与系统交互。构建交互式应用程序的第一步是检测输入手势。通过创建一个简单的按钮来了解其工作原理:
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 使用 StatefulWidget
来体现这一概念。StatefulWidget
是特殊的组件,它们知道如何生成用于保存状态的 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 = 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 = 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
。
键
#当组件重建时,使用键(key)来控制框架如何将组件与其他组件进行匹配。默认情况下,框架根据组件的 runtimeType
及其出现的顺序来匹配当前构建和先前构建中的组件。使用键时,框架要求两个组件具有相同的 key
以及相同的 runtimeType
。
键在构建许多相同类型组件实例的组件中最有用。例如,ShoppingList
组件,它构建足够多的 ShoppingListItem
实例来填充其可见区域:
如果没有键,当前构建中的第一个条目将始终与先前构建中的第一个条目同步,即使从语义上讲,列表中的第一个条目刚刚滚出屏幕并且在视口中不再可见。
通过为列表中的每个条目分配一个“语义”键,无限列表可以更高效,因为框架会同步具有匹配语义键的条目,从而实现相似(或相同)的视觉外观。此外,语义同步条目意味着有状态子组件中保留的状态仍然附着在相同的语义条目上,而不是视口中相同数字位置的条目。
更多信息,请查阅 Key
API。
全局键
#使用全局键(global key)来唯一标识子组件。全局键必须在整个组件层次结构中全局唯一,而局部键只需在同级组件中唯一即可。由于它们是全局唯一的,全局键可用于检索与组件关联的状态。
更多信息,请查阅 GlobalKey
API。