面向 SwiftUI 开发者的 Flutter
学习在构建 Flutter 应用时运用 SwiftUI 开发经验。
想用 Flutter 编写移动应用的 SwiftUI 开发者应阅读本指南,说明如何将现有 SwiftUI 知识应用于 Flutter。
Flutter is a framework for building cross-platform applications that uses the Dart programming language. To understand some differences between programming with Dart and programming with Swift, see Learning Dart as a Swift Developer and Flutter concurrency for Swift developers.
Your SwiftUI knowledge and experience are highly valuable when building with Flutter.
Flutter also makes a number of adaptations to app behavior when running on iOS and macOS. To learn how, see Platform adaptations.
This document can be used as a cookbook by jumping around and finding questions that are most relevant to your needs. This guide embeds sample code. By using the "Open in DartPad" button that appears on hover or focus, you can open and run some of the examples on DartPad.
概览
#As an introduction, watch the following video. It outlines how Flutter works on iOS and how to use Flutter to build iOS apps.
Flutter and SwiftUI code describes how the UI looks and works. Developers call this type of code a declarative framework.
View 与 Widget
#SwiftUI 将 UI 组件表示为 view,通过 modifier 配置 view。
Text("Hello, World!") // <-- This is a View
.padding(10) // <-- This is a modifier of that View
Flutter 将 UI 组件表示为 widget。
view 与 widget 仅在需要变更前存在,称为 immutability(不可变性)。SwiftUI 用 View modifier 表示 UI 组件属性;Flutter 则用 widget 同时表示 UI 组件及其属性。
Padding( // <-- This is a Widget
padding: EdgeInsets.all(10.0), // <-- So is this
child: Text("Hello, World!"), // <-- This, too
)));
组合布局时,SwiftUI 与 Flutter 都嵌套 UI 组件:SwiftUI 嵌套 View,Flutter 嵌套 Widget。
布局过程
#SwiftUI 按以下过程布局 view:
The parent view proposes a size to its child view.
父 view 向子 view 提议尺寸。
-
All subsequent child views:
- propose a size to their child's view
- ask that child what size it wants
所有后续子 view:
- 向 其 子 view 提议尺寸
- 询问子 view 期望尺寸
Each parent view renders its child view at the returned size.
每个父 view 按返回的尺寸渲染子 view。
Flutter 的过程略有不同:
-
The parent widget passes constraints down to its children. Constraints include minimum and maximum values for height and width.
父 widget 向子级传递约束,包括高度与宽度的最小值和最大值。
-
The child tries to decide its size. It repeats the same process with its own list of children:
- It informs its child of the child's constraints.
- It asks its child what size it wishes to be.
子 widget 尝试决定尺寸,对其子级重复相同过程:告知子级约束并询问期望尺寸。
-
The parent lays out the child.
- If the requested size fits in the constraints, the parent uses that size.
- If the requested size doesn't fit in the constraints, the parent limits the height, width, or both to fit in its constraints.
父级布局子级:若请求尺寸符合约束则采用;否则限制高度、宽度或两者以符合约束。
Flutter 与 SwiftUI 不同在于父组件可覆盖子组件期望尺寸;widget 不能任意尺寸,也无法知晓或决定屏幕位置,由父组件决定。
要强制子 widget 以特定尺寸渲染,父级须设置紧约束;最小尺寸等于最大尺寸时为紧约束。
在 SwiftUI 中,view 可扩展到可用空间或限制为内容尺寸。Flutter widget 行为类似。
但 Flutter 父 widget 可提供无界约束,最大值设为无穷。
UnboundedBox(
child: Container(
width: double.infinity, height: double.infinity, color: red),
)
若子级扩展且有无界约束,Flutter 会返回溢出警告:
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
要了解 Flutter 约束,请参阅 Understanding constraints。
设计系统
#Flutter 面向多平台,应用不必遵循特定设计系统。本指南使用 Material widget,但可采用多种设计系统:
Custom Material widgets
自定义 Material widget
Community built widgets
社区构建的 widget
Your own custom widgets
你自己的自定义 widget
-
Cupertino widgets that follow Apple's Human Interface Guidelines
遵循 Apple 人机界面指南的 Cupertino widgets
参考自定义设计系统的优秀应用请参阅 Wonderous。
UI 基础
#本节涵盖 Flutter UI 基础及与 SwiftUI 的对比,包括入门、静态文本、按钮、点击响应、列表与网格等。
入门
#在 SwiftUI 中,用 App 启动应用。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
HomePage()
}
}
}
另一常见做法将应用 body 放在符合 View 协议的 struct 中,如下:
struct HomePage: View {
var body: some View {
Text("Hello, World!")
}
}
启动 Flutter 应用时,将应用实例传给 runApp。
void main() {
runApp(const MyApp());
}
App 是 widget,build 方法描述所代表的用户界面。通常以 WidgetApp
类(如 CupertinoApp)开始。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Returns a CupertinoApp that, by default,
// has the look and feel of an iOS app.
return const CupertinoApp(home: HomePage());
}
}
HomePage 中的 widget 可能以 Scaffold 开始,实现应用基本布局结构。
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(body: Center(child: Text('Hello, World!')));
}
}
注意 Flutter 使用 Center。SwiftUI 默认将 view 内容居中渲染,Flutter 并非总是如此;Scaffold
不会将 body 居中。要居中文本请用 Center 包裹,详见 Widget catalog。
添加按钮
#在 SwiftUI 中,用 Button 结构体创建按钮。
Button("Do something") {
// this closure gets called when your
// button is tapped
}
在 Flutter 中,用 CupertinoButton 类达到相同效果:
CupertinoButton(
onPressed: () {
// This closure is called when your button is tapped.
},
const Text('Do something'),
),
Flutter 提供多种预定义样式按钮。CupertinoButton
来自 Cupertino 库,其 widget 使用 Apple 设计系统。
水平对齐组件
#在 SwiftUI 中,stack view 在布局中很重要,有两种结构:
HStackfor horizontal stack viewsHStack用于水平 stackVStackfor vertical stack viewsVStack用于垂直 stack
以下 SwiftUI view 在水平 stack 中添加地球图标与文本:
HStack {
Image(systemName: "globe")
Text("Hello, world!")
}
Flutter 使用 Row
而非 HStack:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(CupertinoIcons.globe), Text('Hello, world!')],
),
Row 的 children 需 List<Widget>;mainAxisAlignment
控制额外空间中的子项位置,MainAxisAlignment.center 将子项放在主轴中心;Row 的主轴为水平轴。
垂直对齐组件
#以下示例建立在上一节基础上。
在 SwiftUI 中,用 VStack 将组件垂直排列。
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
Flutter 使用与上一示例相同的 Dart 代码,但将 Row
换为 Column:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(CupertinoIcons.globe), Text('Hello, world!')],
),
显示列表视图
#
在 SwiftUI 中,用 List 显示项序列;要显示模型对象序列,须使用户能识别模型对象,对象需符合 Identifiable
协议。
struct Person: Identifiable {
var name: String
}
var persons = [
Person(name: "Person 1"),
Person(name: "Person 2"),
Person(name: "Person 3"),
]
struct ListWithPersons: View {
let persons: [Person]
var body: some View {
List {
ForEach(persons) { person in
Text(person.name)
}
}
}
}
这与 Flutter 构建列表 widget 的方式类似;Flutter 不要求列表项可识别,你设置项数并为每项构建 widget。
class Person {
String name;
Person(this.name);
}
final List<Person> items = [
Person('Person 1'),
Person('Person 2'),
Person('Person 3'),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index].name));
},
),
);
}
}
Flutter 列表有一些注意事项:
-
The
ListViewwidget has a builder method. This works like theForEachwithin SwiftUI'sListstruct. -
The
itemCountparameter of theListViewsets how many items theListViewdisplays. -
The
itemBuilderhas an index parameter that will be between zero and one less than itemCount.
The previous example returned a ListTile
widget for each item.
The ListTile widget includes properties like height and
font-size.
These properties help build a list. However, Flutter allows you to return
almost any widget that represents your data.
显示网格
#在 SwiftUI 中构建非条件网格时,使用 Grid 与 GridRow。
Grid {
GridRow {
Text("Row 1")
Image(systemName: "square.and.arrow.down")
Image(systemName: "square.and.arrow.up")
}
GridRow {
Text("Row 2")
Image(systemName: "square.and.arrow.down")
Image(systemName: "square.and.arrow.up")
}
}
在 Flutter 中用 GridView
widget 显示网格,有多种构造函数,以下使用 .builder() 初始化:
const widgets = <Widget>[
Text('Row 1'),
Icon(CupertinoIcons.arrow_down_square),
Icon(CupertinoIcons.arrow_up_square),
Text('Row 2'),
Icon(CupertinoIcons.arrow_down_square),
Icon(CupertinoIcons.arrow_up_square),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisExtent: 40,
),
itemCount: widgets.length,
itemBuilder: (context, index) => widgets[index],
),
);
}
}
The SliverGridDelegateWithFixedCrossAxisCount delegate determines
various parameters that the grid uses to lay out its components.
This includes crossAxisCount that dictates the number of items
displayed on each row.
SwiftUI 的 Grid 与 Flutter 的 GridView 的区别在于 Grid 需要 GridRow;GridView
用 delegate 决定布局。
创建滚动视图
#
在 SwiftUI 中,用 ScrollView 创建自定义滚动组件,以下示例以可滚动方式显示一系列 PersonView。
ScrollView {
VStack(alignment: .leading) {
ForEach(persons) { person in
PersonView(person: person)
}
}
}
Flutter 用 SingleChildScrollView
创建滚动视图,以下 mockPerson 模拟 Person 实例创建 PersonView。
SingleChildScrollView(
child: Column(
children: mockPersons
.map((person) => PersonView(person: person))
.toList(),
),
),
响应式与自适应设计
#在 SwiftUI 中,用 GeometryReader 创建相对 view 尺寸。
例如,你可以:
Multiply
geometry.size.widthby some factor to set the width.将
geometry.size.width乘以某因子设置 width(宽度)。Use
GeometryReaderas a breakpoint to change the design of your app.将
GeometryReader用作断点以更改应用设计。
还可用 horizontalSizeClass 查看 size class 为 .regular 或 .compact。
在 Flutter 中创建相对视图有两种方式:
-
Get the
BoxConstraintsobject in theLayoutBuilderclass. -
Use the
MediaQuery.of()in your build functions to get the size and orientation of your current app.
To learn more, check out Creating responsive and adaptive apps.
管理状态
#在 SwiftUI 中,用 @State 属性包装器表示 SwiftUI view 的内部状态。
struct ContentView: View {
@State private var counter = 0;
var body: some View {
VStack{
Button("+") { counter+=1 }
Text(String(counter))
}
}}
SwiftUI 还有 ObservableObject 等更复杂状态管理选项。
Flutter manages local state using a StatefulWidget.
Implement a stateful widget with the following two classes:
- a subclass of
StatefulWidget - a subclass of
State
The State object stores the widget's state.
To change a widget's state, call setState() from the State
subclass
to tell the framework to redraw the widget.
The following example shows a part of a counter app:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_counter'),
TextButton(
onPressed: () => setState(() {
_counter++;
}),
child: const Text('+'),
),
],
),
),
);
}
}
To learn more ways to manage state, check out State management.
动画
#UI 动画主要有两类。
Implicit that animated from a current value to a new target.
隐式:从当前值动画到新目标。
Explicit that animates when asked.
显式:按需动画。
隐式动画
#SwiftUI 与 Flutter 动画方式相似,都指定 duration、curve 等参数。
在 SwiftUI 中,用 animate() modifier 处理隐式动画。
Button("Tap me!"){
angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1))
Flutter 有隐式动画 widget,简化常见 widget 动画,命名格式为 AnimatedFoo。
例如旋转按钮用 AnimatedRotation,为
Transform.rotate widget 添加动画。
AnimatedRotation(
duration: const Duration(seconds: 1),
turns: turns,
curve: Curves.easeIn,
TextButton(
onPressed: () {
setState(() {
turns += .125;
});
},
const Text('Tap me!'),
),
),
Flutter 可创建自定义隐式动画,用 TweenAnimationBuilder
组合新动画 widget。
显式动画
#显式动画方面,SwiftUI 用 withAnimation()。
Flutter 有显式动画 widget,命名如 FooTransition,例如 RotationTransition。
Flutter 还可用 AnimatedWidget 或 AnimatedBuilder 创建自定义显式动画。
更多动画信息请参阅 Animations overview。
在屏幕上绘制
#在 SwiftUI 中,用 CoreGraphics 在屏幕上绘制线条与形状。
Flutter 基于 Canvas 类提供 API,有两个辅助类:
-
CustomPaintthat requires a painter:dartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ), -
CustomPainterthat implements your algorithm to draw to the canvas.dartclass SignaturePainter extends CustomPainter { SignaturePainter(this.points); final List<Offset?> points; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5; for (int i = 0; i < points.length - 1; i++) { if (points[i] != null && points[i + 1] != null) { canvas.drawLine(points[i]!, points[i + 1]!, paint); } } } @override bool shouldRepaint(SignaturePainter oldDelegate) => oldDelegate.points != points; }
导航
#本节说明应用页面间导航、push/pop 机制等。
在页面间导航
#开发者用称为 navigation routes(导航路由)的不同页面构建 iOS 与 macOS 应用。
在 SwiftUI 中,NavigationStack 表示该页面栈。
以下示例创建显示人员列表的应用,点击人员在新的导航链接中显示详情。
NavigationStack(path: $path) {
List {
ForEach(persons) { person in
NavigationLink(
person.name,
value: person
)
}
}
.navigationDestination(for: Person.self) { person in
PersonView(person: person)
}
}
若无复杂链接的小型 Flutter 应用,可用 Navigator
命名路由;定义路由后按名称调用。
-
Name each route in the class passed to the
runApp()function. The following example usesApp: -
在传给
runApp()的类中命名每条路由,以下示例使用App:dart// Defines the route name as a constant // so that it's reusable. const detailsPageRouteName = '/details'; class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return CupertinoApp( home: const HomePage(), // The [routes] property defines the available named routes // and the widgets to build when navigating to those routes. routes: {detailsPageRouteName: (context) => const DetailsPage()}, ); } }The following sample generates a list of persons using
mockPersons(). Tapping a person pushes the person's detail page to theNavigatorusingpushNamed().dartListView.builder( itemCount: mockPersons.length, itemBuilder: (context, index) { final person = mockPersons.elementAt(index); final age = '${person.age} years old'; return ListTile( title: Text(person.name), subtitle: Text(age), trailing: const Icon(Icons.arrow_forward_ios), onTap: () { // When a [ListTile] that represents a person is // tapped, push the detailsPageRouteName route // to the Navigator and pass the person's instance // to the route. Navigator.of( context, ).pushNamed(detailsPageRouteName, arguments: person); }, ); }, ), -
Define the
DetailsPagewidget that displays the details of each person. In Flutter, you can pass arguments into the widget when navigating to the new route. Extract the arguments usingModalRoute.of(): -
定义显示每人详情的
DetailsPagewidget;导航到新路由时可传参,用ModalRoute.of()提取。dartclass DetailsPage extends StatelessWidget { const DetailsPage({super.key}); @override Widget build(BuildContext context) { // Read the person instance from the arguments. final Person person = ModalRoute.of(context)?.settings.arguments as Person; // Extract the age. final age = '${person.age} years old'; return Scaffold( // Display name and age. body: Column(children: [Text(person.name), Text(age)]), ); } }
更高级导航需求可使用 go_router 等路由包。
更多内容请参阅 Navigation and routing。
手动返回
#在 SwiftUI 中,用 dismiss 环境值返回上一屏。
Button("Pop back") {
dismiss()
}
在 Flutter 中,用 Navigator 类的 pop():
TextButton(
onPressed: () {
// This code allows the
// view to pop back to its presenter.
Navigator.of(context).pop();
},
child: const Text('Pop back'),
),
导航到其他应用
#在 SwiftUI 中,用 openURL 环境变量打开其他应用的 URL。
@Environment(\.openURL) private var openUrl
// View code goes here
Button("Open website") {
openUrl(
URL(
string: "https://google.com"
)!
)
}
在 Flutter 中,使用 url_launcher
插件。
CupertinoButton(
onPressed: () async {
await launchUrl(Uri.parse('https://google.com'));
},
const Text('Open website'),
),
主题、样式与媒体
#可轻松设置 Flutter 应用样式,包括主题切换、文本与 UI 组件设计等。
使用深色模式
#在 SwiftUI 中,在 View 上调用 preferredColorScheme() 使用深色模式。
在 Flutter 中,可在应用级用 App 的 theme 控制亮度模式。
const CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.dark),
home: HomePage(),
);
设置文本样式
#在 SwiftUI 中,用 modifier 设置文本样式,例如用 font() 修改 Text 字体。
Text("Hello, world!")
.font(.system(size: 30, weight: .heavy))
.foregroundColor(.yellow)
在 Flutter 中,将 TextStyle 作为 Text 的 style 参数。
Text(
'Hello, world!',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: CupertinoColors.systemYellow,
),
),
设置按钮样式
#在 SwiftUI 中,用 modifier 设置按钮样式。
Button("Do something") {
// Do something when the button is tapped.
}
.font(.system(size: 30, weight: .bold))
.background(Color.yellow)
.foregroundColor(Color.blue)
在 Flutter 中,设置子项样式或修改按钮属性。
在以下示例中:
The
colorproperty ofCupertinoButtonsets itscolor.CupertinoButton的color设置其颜色。-
The
colorproperty of the childTextwidget sets the button text color. 子
Textwidget 的color设置按钮文字颜色。
child: CupertinoButton(
color: CupertinoColors.systemYellow,
onPressed: () {},
child: const Text(
'Do something',
style: TextStyle(
color: CupertinoColors.systemBlue,
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
使用自定义字体
#在 SwiftUI 中,两步使用自定义字体:将字体文件加入项目,再用 .font() modifier 应用到 UI 组件。
Text("Hello")
.font(
Font.custom(
"BungeeSpice-Regular",
size: 40
)
)
在 Flutter 中,用 pubspec.yaml 管理平台无关的资源。添加自定义字体步骤:
-
Create a folder called
fontsin the project's root directory. This optional step helps to organize your fonts. 在项目根目录创建
fonts文件夹(可选,便于组织字体)。-
Add your
.ttf,.otf, or.ttcfont file into thefontsfolder. 将
.ttf、.otf或.ttc字体文件放入fonts文件夹。Open the
pubspec.yamlfile within the project.打开项目中的
pubspec.yaml。Find the
fluttersection.找到
flutter节。Add your custom font(s) under the
fontssection.-
在
fonts节下添加自定义字体。yamlflutter: fonts: - family: BungeeSpice fonts: - asset: fonts/BungeeSpice-Regular.ttf
添加字体后,可如下使用:
Text(
'Cupertino',
style: TextStyle(fontSize: 40, fontFamily: 'BungeeSpice'),
),
在应用中打包图片
#在 SwiftUI 中,先将图像加入 Assets.xcassets,再用 Image view 显示。
在 Flutter 中添加图像的方式类似自定义字体。
Add an
imagesfolder to the root directory.在根目录添加
images文件夹。Add this asset to the
pubspec.yamlfile.-
在
pubspec.yaml中添加该资源。yamlflutter: assets: - images/Blueberries.jpg
添加图像后,用 Image widget 的 .asset() 构造函数显示。该构造函数:
Instantiates the given image using the provided path.
用提供的路径实例化给定图像。
Reads the image from the assets bundled with your app.
从与应用捆绑的资源读取图像。
Displays the image on the screen.
在屏幕上显示图像。
完整示例请参阅 Image 文档。
在应用中打包视频
#在 SwiftUI 中,两步捆绑本地视频:导入 AVKit,再实例化 VideoPlayer view。
在 Flutter 中,添加 video_player 插件,可从同一代码库在 Android、iOS 与 Web 上播放视频。
Add the plugin to your app and add the video file to your project.
将插件加入应用并添加视频文件。
Add the asset to your
pubspec.yamlfile.在
pubspec.yaml中添加资源。-
Use the
VideoPlayerControllerclass to load and play your video file. 用
VideoPlayerController加载并播放视频。
完整 walkthrough 请参阅 video_player example。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。