Flutter for 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 的外观和工作方式。开发者将这种类型的代码称为*声明式框架*。
视图与组件
#**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
垂直对齐组件
#以下示例基于上一节中的示例。
在 **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` 组件有一个构建器方法。这与 SwiftUI 的 `List` 结构体中的 `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` 属性设置其*颜色*。
- 子 `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 示例。