Flutter for SwiftUI Developers
学习如何在构建 Flutter 应用时应用 SwiftUI 开发人员的知识。
希望使用 Flutter 编写移动应用的 SwiftUI 开发人员应查阅本指南。它解释了如何将现有的 SwiftUI 知识应用于 Flutter。
Flutter 是一个用于构建跨平台应用程序的框架,它使用 Dart 编程语言。要了解 Dart 编程与 Swift 编程之间的一些区别,请参阅 Learning Dart as a Swift Developer 和 Flutter concurrency for Swift developers。
您的 SwiftUI 知识和经验在构建 Flutter 应用时非常有价值。
Flutter 在在 iOS 和 macOS 上运行时也会对应用行为进行一些调整。要了解如何操作,请参阅 Platform adaptations。
本文档可以用作食谱,通过跳跃查找与您的需求最相关的问题。本指南嵌入了示例代码。通过使用悬停或焦点时出现的“在 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 组件属性表示为 View 修饰符。相比之下,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 中如何工作,请参阅 Understanding constraints。
设计系统
#由于 Flutter 定位多个平台,您的应用不需要符合任何设计系统。虽然本指南介绍了 Material 部件,但您的 Flutter 应用可以使用许多不同的设计系统
- 自定义 Material 部件
- 社区构建的部件
- 您自己的自定义部件
- Cupertino widgets 遵循 Apple 的 Human Interface Guidelines
如果您正在寻找一个具有自定义设计系统的优秀参考应用,请查看 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 部件中。要了解不同的部件及其默认行为,请查看 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 库。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部件有一个 builder 方法。这与 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 管理局部状态。使用以下两个类实现一个有状态的 Widget
StatefulWidget的子类State的子类
State 对象存储 Widget 的状态。要更改 Widget 的状态,请从 State 子类调用 setState(),以告知框架重新绘制 Widget。
以下示例显示了一个计数器应用的一部分
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 包含用于隐式动画的 Widget。这简化了常见 Widget 的动画处理。Flutter 将这些 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 允许你创建自定义隐式动画。要组合一个新的动画 Widget,请使用 TweenAnimationBuilder。
显式动画
#对于显式动画,SwiftUI 使用 withAnimation() 函数。
Flutter 包含命名格式为 FooTransition 的显式动画 Widget。一个例子是 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()函数的类中命名每个路由。以下示例使用Appdart// 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()生成人员列表。点击人员会将人员的详细信息页面推送到Navigator,使用pushNamed()。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); }, ); }, ), -
定义
DetailsPageWidget,它显示每个人的详细信息。在 Flutter 中,你可以在导航到新路由时将参数传递到 Widget 中。使用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 Widget 作为 Text Widget 的 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 中的按钮 Widget 样式,请设置其子项的样式,或修改按钮本身上的属性。
在以下示例中
CupertinoButton的color属性设置其color。- 子
TextWidget 的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 Widget 的 .asset() 构造函数显示它。此构造函数
- 使用提供的路径实例化给定的图像。
- 从与你的应用程序捆绑的资源中读取图像。
- 在屏幕上显示图像。
要查看完整的示例,请查看 Image 文档。
在应用中捆绑视频
#在 SwiftUI 中,你将本地视频文件与你的应用程序捆绑在一起,分两个步骤。首先,你导入 AVKit 框架,然后你实例化一个 VideoPlayer 视图。
在 Flutter 中,将 video_player 插件添加到你的项目中。此插件允许你从相同的代码库创建在 Android、iOS 和 Web 上工作的视频播放器。
- 将插件添加到你的应用程序并将视频文件添加到你的项目中。
- 将资源添加到你的
pubspec.yaml文件。 - 使用
VideoPlayerController类加载和播放你的视频文件。
要查看完整的演练,请查看 video_player 示例。