Flutter 开发者指南:致 SwiftUI 开发者
希望使用 Flutter 编写移动应用的 SwiftUI 开发者应查阅本指南。本指南解释了如何将现有的 SwiftUI 知识应用于 Flutter。
Flutter 是一个用于构建跨平台应用的框架,它使用 Dart 编程语言。要了解 Dart 编程与 Swift 编程之间的一些差异,请参阅Swift 开发者学习 Dart 和Swift 开发者了解 Flutter 并发。
在用 Flutter 进行构建时,你的 SwiftUI 知识和经验非常有价值。
Flutter 还在 iOS 和 macOS 上运行时对应用行为进行了多项调整。要了解具体方法,请参阅平台适配。
本文档可以用作一本食谱,你可以跳到最符合你需求的问题。本指南嵌入了示例代码。通过使用悬停或聚焦时出现的“在 DartPad 中打开”按钮,你可以在 DartPad 上打开并运行一些示例。
概述
#作为介绍,请观看以下视频。它概述了 Flutter 在 iOS 上的工作原理以及如何使用 Flutter 构建 iOS 应用。
Flutter 和 SwiftUI 代码描述了 UI 的外观和工作方式。开发者将这种类型的代码称为*声明式框架*。
视图 vs. 小部件
#**SwiftUI** 将 UI 组件表示为*视图*。你使用*修饰符*配置视图。
Text("Hello, World!") // <-- This is a View
.padding(10) // <-- This is a modifier of that View
**Flutter** 将 UI 组件表示为*小部件*。
视图和小部件都只存在到需要更改时。这些语言将此属性称为*不变性*。SwiftUI 将 UI 组件属性表示为视图修饰符。相比之下,Flutter 将小部件用于 UI 组件及其属性。
Padding( // <-- This is a Widget
padding: EdgeInsets.all(10.0), // <-- So is this
child: Text("Hello, World!"), // <-- This, too
)));
为了组合布局,SwiftUI 和 Flutter 都将 UI 组件嵌套在一起。SwiftUI 嵌套视图,而 Flutter 嵌套小部件。
布局过程
#**SwiftUI** 使用以下过程来布局视图
- 父视图向其子视图建议一个大小。
- 所有后续子视图
- 向*它们*的子视图建议一个大小
- 询问该子视图它想要什么大小
- 每个父视图以返回的大小渲染其子视图。
**Flutter** 的过程有所不同
父小部件将约束传递给其子小部件。约束包括高度和宽度的最小值和最大值。
子小部件尝试决定其大小。它对其自己的子小部件列表重复相同的过程
- 它告知其子小部件子小部件的约束。
- 它询问其子小部件它希望是什么大小。
父小部件布局子小部件。
- 如果请求的大小符合约束,则父小部件使用该大小。
- 如果请求的大小不符合约束,则父小部件限制高度、宽度或两者以适应其约束。
Flutter 与 SwiftUI 不同,因为父组件可以覆盖子组件的所需大小。小部件不能拥有任何它想要的大小。它也无法知道或决定它在屏幕上的位置,因为其父组件做出该决定。
要强制子小部件以特定大小渲染,父小部件必须设置紧密约束。当其约束的最小大小值等于其最大大小值时,约束变为紧密。
在 **SwiftUI** 中,视图可能会扩展到可用空间或将其大小限制为其内容的大小。**Flutter** 小部件的行为方式类似。
然而,在 Flutter 中,父小部件可以提供无边界约束。无边界约束将其最大值设置为无穷大。
UnboundedBox(
child: Container(
width: double.infinity, height: double.infinity, color: red),
)
如果子小部件扩展且具有无边界约束,Flutter 将返回溢出警告
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)

要了解 Flutter 中约束的工作原理,请参阅理解约束。
设计系统
#因为 Flutter 针对多个平台,所以你的应用不需要符合任何设计系统。尽管本指南以 Material 小部件为特色,但你的 Flutter 应用可以使用许多不同的设计系统
- 自定义 Material 小部件
- 社区构建的小部件
- 你自己的自定义小部件
- 遵循 Apple 人机界面指南的Cupertino 小部件
在新标签页中观看 YouTube 视频:“Flutter's cupertino library for iOS developers”
如果你正在寻找一个具有自定义设计系统的优秀参考应用,请查看 Wonderous。
UI 基础
#本节涵盖 Flutter 中 UI 开发的基础知识,以及它与 SwiftUI 的比较。这包括如何开始开发应用、显示静态文本、创建按钮、响应按压事件、显示列表、网格等。
入门
#在 **SwiftUI** 中,你使用 App
来启动你的应用。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
HomePage()
}
}
}
另一个常见的 SwiftUI 实践是将应用主体放在符合 View
协议的 struct
中,如下所示
struct HomePage: View {
var body: some View {
Text("Hello, World!")
}
}
要启动你的 **Flutter** 应用,将你的应用实例传递给 runApp
函数。
void main() {
runApp(const MyApp());
}
App
是一个小部件。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
中使用的小部件可能以 Scaffold
类开始。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 默认将其视图内容渲染在其中心。Flutter 则并非总是如此。Scaffold
不会在屏幕中心渲染其 body
小部件。要使文本居中,请将其包装在 Center
小部件中。要了解不同小部件及其默认行为,请查看小部件目录。
添加按钮
#在 **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 库。Cupertino 库中的小部件使用 Apple 的设计系统。
水平对齐组件
#在 **SwiftUI** 中,堆栈视图在设计布局中扮演着重要角色。两个独立的结构允许你创建堆栈
HStack
用于水平堆栈视图VStack
用于垂直堆栈视图
以下 SwiftUI 视图将一个地球图像和文本添加到水平堆栈视图中
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
属性告诉 Flutter 如何在额外空间中定位子小部件。MainAxisAlignment.center
将子小部件定位在主轴的中心。对于 Row
,主轴是水平轴。
垂直对齐组件
#以下示例基于上一节中的示例。
在 **SwiftUI** 中,你使用 VStack
将组件排列成垂直柱。
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
**Flutter** 使用与上一个示例相同的 Dart 代码,只是它将 Column
替换为 Row
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** 倾向于构建其列表小部件的方式。Flutter 不需要列表项是可识别的。你设置要显示的项目数量,然后为每个项目构建一个小部件。
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 的列表有一些注意事项
ListView
小部件有一个构建器方法。这类似于 SwiftUIList
结构体中的ForEach
。ListView
的itemCount
参数设置ListView
显示的项目数量。itemBuilder
有一个索引参数,该参数将在零到 itemCount 减一之间。
前面的示例为每个项目返回了一个 ListTile
小部件。ListTile
小部件包括 height
和 font-size
等属性。这些属性有助于构建列表。但是,Flutter 允许你返回几乎任何代表你数据的小部件。
显示网格
#在 **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
小部件。此小部件有各种构造函数。每个构造函数都有相似的目标,但使用不同的输入参数。以下示例使用 .builder()
初始化器
const widgets = [
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],
),
);
}
}
SliverGridDelegateWithFixedCrossAxisCount
委托确定网格用于布局其组件的各种参数。这包括 crossAxisCount
,它指定每行显示的项目数量。
SwiftUI 的 Grid
和 Flutter 的 GridView
的区别在于 Grid
需要 GridRow
。GridView
使用委托来决定网格应该如何布局其组件。
创建滚动视图
#在 **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
来创建相对视图大小。
例如,你可以
- 将
geometry.size.width
乘以某个因子来设置*宽度*。 - 使用
GeometryReader
作为断点来改变应用的设计。
你还可以使用 horizontalSizeClass
查看大小类是否为 .regular
或 .compact
。
要在 **Flutter** 中创建相对视图,你可以使用以下两种选项之一
- 在
LayoutBuilder
类中获取BoxConstraints
对象。 - 在你的构建函数中使用
MediaQuery.of()
来获取当前应用的大小和方向。
要了解更多信息,请查看创建响应式和自适应应用。
状态管理
#在 **SwiftUI** 中,你使用 @State
属性包装器来表示 SwiftUI 视图的内部状态。
struct ContentView: View {
@State private var counter = 0;
var body: some View {
VStack{
Button("+") { counter+=1 }
Text(String(counter))
}
}}
**SwiftUI** 还包括几种用于更复杂状态管理的选项,例如 ObservableObject
协议。
**Flutter** 使用 StatefulWidget
管理本地状态。使用以下两个类实现有状态小部件
StatefulWidget
的子类State
的子类
State
对象存储小部件的状态。要更改小部件的状态,从 State
子类调用 setState()
以告知框架重新绘制小部件。
以下示例展示了计数器应用的一部分
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('+'),
),
],
),
),
);
}
}
要了解更多管理状态的方法,请查看状态管理。
动画
#存在两种主要类型的 UI 动画。
- 隐式动画:从当前值动画到新目标。
- 显式动画:在被要求时进行动画。
隐式动画
#SwiftUI 和 Flutter 采用相似的动画方法。在这两个框架中,你都指定了诸如 duration
和 curve
之类的参数。
在 **SwiftUI** 中,你使用 animate()
修饰符来处理隐式动画。
Button("Tap me!"){
angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1))
**Flutter** 包含用于隐式动画的小部件。这简化了常见小部件的动画。Flutter 以以下格式命名这些小部件:AnimatedFoo
。
例如:要旋转按钮,请使用 AnimatedRotation
类。这将对 Transform.rotate
小部件进行动画处理。
AnimatedRotation(
duration: const Duration(seconds: 1),
turns: turns,
curve: Curves.easeIn,
TextButton(
onPressed: () {
setState(() {
turns += .125;
});
},
const Text('Tap me!'),
),
),
Flutter 允许你创建自定义隐式动画。要组合新的动画小部件,请使用 TweenAnimationBuilder
。
显式动画
#对于显式动画,**SwiftUI** 使用 withAnimation()
函数。
**Flutter** 包含名称格式为 FooTransition
的显式动画小部件。一个示例是 RotationTransition
类。
Flutter 还允许你使用 AnimatedWidget
或 AnimatedBuilder
创建自定义显式动画。
要了解有关 Flutter 中动画的更多信息,请参阅动画概述。
在屏幕上绘图
#在 **SwiftUI** 中,你使用 CoreGraphics
在屏幕上绘制线条和形状。
**Flutter** 有一个基于 Canvas
类的 API,其中有两个类可以帮助你绘制
需要绘制器的
CustomPaint
dartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ),
实现你的算法以绘制到画布的
CustomPainter
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; }
导航
#本节解释了如何在应用页面之间导航、推入和弹出机制等等。
页面导航
#开发者使用称为*导航路由*的不同页面构建 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
和命名路由。定义导航路由后,使用它们的名称调用你的导航路由。
在传递给
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()}, ); } }
以下示例使用
mockPersons()
生成人员列表。点击一个人会将该人员的详细信息页面使用pushNamed()
推入Navigator
。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); }, ); }, ),
定义显示每个人详细信息的
DetailsPage
小部件。在 Flutter 中,你可以在导航到新路由时将参数传递到小部件中。使用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。
要了解更多信息,请查看导航和路由。
手动返回
#在 **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** 中,你使用修饰符函数来设置文本样式。例如,要更改 Text
字符串的字体,请使用 font()
修饰符
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** 中,你使用修饰符函数来设置按钮样式。
Button("Do something") {
// do something when button is tapped
}
.font(.system(size: 30, weight: .bold))
.background(Color.yellow)
.foregroundColor(Color.blue)
}
要在 **Flutter** 中设置按钮小部件的样式,请设置其子项的样式,或修改按钮本身的属性。
在以下示例中
CupertinoButton
的color
属性设置其color
。- 子
Text
小部件的color
属性设置按钮文本颜色。
child: CupertinoButton(
color: CupertinoColors.systemYellow,
onPressed: () {},
padding: const EdgeInsets.all(16),
child: const Text(
'Do something',
style: TextStyle(
color: CupertinoColors.systemBlue,
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
使用自定义字体
#在 **SwiftUI** 中,你可以通过两个步骤在应用中使用自定义字体。首先,将字体文件添加到你的 SwiftUI 项目中。添加文件后,使用 .font()
修饰符将其应用于你的 UI 组件。
Text("Hello")
.font(
Font.custom(
"BungeeSpice-Regular",
size: 40
)
)
在 **Flutter** 中,你使用名为 pubspec.yaml
的文件控制你的资源。此文件与平台无关。要将自定义字体添加到你的项目,请按照以下步骤操作
在项目的根目录中创建一个名为
fonts
的文件夹。此可选步骤有助于组织你的字体。将你的
.ttf
、.otf
或.ttc
字体文件添加到fonts
文件夹中。打开项目中的
pubspec.yaml
文件。找到
flutter
部分。在
fonts
部分下添加你的自定义字体。yamlflutter: fonts: - family: BungeeSpice fonts: - asset: fonts/BungeeSpice-Regular.ttf
将字体添加到你的项目后,你可以按照以下示例使用它
Text(
'Cupertino',
style: TextStyle(fontSize: 40, fontFamily: 'BungeeSpice'),
),
在应用中捆绑图片
#在 **SwiftUI** 中,你首先将图像文件添加到 Assets.xcassets
,然后使用 Image
视图来显示图像。
要在 **Flutter** 中添加图像,请遵循与添加自定义字体类似的方法。
在根目录中添加一个
images
文件夹。将此资产添加到
pubspec.yaml
文件。yamlflutter: assets: - images/Blueberries.jpg
添加图像后,使用 Image
小部件的 .asset()
构造函数显示它。此构造函数
- 使用提供的路径实例化给定的图像。
- 从与你的应用捆绑的资产中读取图像。
- 在屏幕上显示图像。
要查看完整示例,请查看 Image
文档。
在应用中捆绑视频
#在 **SwiftUI** 中,你通过两个步骤将本地视频文件捆绑到你的应用中。首先,你导入 AVKit
框架,然后实例化一个 VideoPlayer
视图。
在 **Flutter** 中,将 video_player 插件添加到你的项目。此插件允许你创建一个可在 Android、iOS 和 Web 上使用相同代码库运行的视频播放器。
- 将插件添加到你的应用并将视频文件添加到你的项目。
- 将资产添加到你的
pubspec.yaml
文件。 - 使用
VideoPlayerController
类加载和播放你的视频文件。
要查看完整的演练,请查看 video_player 示例。