面向 SwiftUI 开发者的 Flutter
学习如何将 SwiftUI 开发知识应用于构建 Flutter 应用。
想要使用 Flutter 编写移动应用的 SwiftUI 开发者应该阅读本指南。它解释了如何将现有的 SwiftUI 知识应用到 Flutter 中。
Flutter 是一个使用 Dart 编程语言构建跨平台应用的框架。要了解 Dart 编程与 Swift 编程之间的一些差异,请参阅 面向 Swift 开发者的 Dart 学习指南 和 面向 Swift 开发者的 Flutter 并发编程。
你在 SwiftUI 方面的知识和经验在构建 Flutter 应用时非常有价值。
Flutter 在 iOS 和 macOS 上运行时也对应用行为进行了一些调整。要了解具体方式,请参阅 平台适配。
本文档可用作手册,通过跳跃阅读找到与你需求最相关的问题。本指南嵌入了示例代码。使用悬停或聚焦时出现的“在 DartPad 中打开”按钮,你可以在 DartPad 中打开并运行部分示例。
概述
#作为入门,请观看以下视频。它概述了 Flutter 如何在 iOS 上工作以及如何使用 Flutter 构建 iOS 应用。
Flutter 和 SwiftUI 代码描述了 UI 的外观和工作方式。开发者将此类代码称为声明式框架。
视图 (Views) vs. 组件 (Widgets)
#SwiftUI 将 UI 组件表示为视图 (views)。你可以使用修饰符 (modifiers) 来配置视图。
Text("Hello, World!") // <-- This is a View
.padding(10) // <-- This is a modifier of that View
Flutter 将 UI 组件表示为组件 (widgets)。
视图和组件都只存在于需要改变之前。这些语言将此属性称为不可变性 (immutability)。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 组件
- 社区构建的组件
- 你自己的自定义组件
- Cupertino 组件(遵循 Apple 的《人机界面指南》)
如果你正在寻找一个采用自定义设计系统的优秀参考应用,请查看 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 代码,只是将 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 首选构建其列表组件的方式。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组件有一个 builder 方法。这类似于 SwiftUI 的List结构体中的ForEach。 -
ListView的itemCount参数设置ListView显示多少项。 -
itemBuilder有一个 index 参数,其范围在 0 到 itemCount 减 1 之间。
前面的示例为每一项返回一个 ListTile 组件。ListTile 组件包含 height 和 font-size 等属性。这些属性有助于构建列表。但是,Flutter 允许你返回几乎任何代表你的数据的组件。
显示网格
#在 SwiftUI 中构建非条件网格时,你使用带有 GridRow 的 Grid。
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 = <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],
),
);
}
}
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(需要一个 painter)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; }
导航
#本节解释如何在应用页面之间导航、推送 (push) 和弹出 (pop) 机制等。
页面间导航
#开发者使用不同的页面构建 iOS 和 macOS 应用,这些页面称为导航路由 (navigation routes)。
在 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 the button is tapped.
}
.font(.system(size: 30, weight: .bold))
.background(Color.yellow)
.foregroundColor(Color.blue)
要在 Flutter 中设置按钮组件的样式,请设置其子组件的样式,或直接修改按钮本身的属性。
在以下示例中:
CupertinoButton的color属性设置其颜色。- 子组件
Text的color属性设置按钮文本的颜色。
child: CupertinoButton(
color: CupertinoColors.systemYellow,
onPressed: () {},
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 示例。