适用于 SwiftUI 开发者的 Flutter
想要使用 Flutter 编写移动应用程序的 SwiftUI 开发者应查看本指南。它解释了如何将现有的 SwiftUI 知识应用于 Flutter。
Flutter 是一个使用 Dart 编程语言构建跨平台应用程序的框架。要了解使用 Dart 编程和使用 Swift 编程之间的一些差异,请参阅 Swift 开发者学习 Dart 和 适用于 Swift 开发者的 Flutter 并发性。
在使用 Flutter 构建时,您的 SwiftUI 知识和经验非常宝贵。
Flutter 还对在 iOS 和 macOS 上运行时的应用程序行为进行了许多调整。要了解如何进行调整,请参阅 平台调整。
本指南可作为食谱使用,您可以随意浏览,找到与您的需求最相关的疑问。本指南嵌入了示例代码。您可以在 DartPad 上测试完整的示例,或在 GitHub 上查看它们。
概述
作为介绍,请观看以下视频。它概述了 Flutter 在 iOS 上的工作原理以及如何使用 Flutter 构建 iOS 应用程序。
Flutter 和 SwiftUI 代码描述了 UI 的外观和工作方式。开发者将这种类型的代码称为声明性框架。
视图与小组件
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 小组件
- 社区构建的小组件
- 您自己的自定义小组件
- 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.
},
child: 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
组件需要 List<Widget>
在 children
参数中。 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);
}
var 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
有一个索引参数,该参数介于 0 和 itemCount 减 1 之间。
上一个示例为每个项目返回一个 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,
child: TextButton(
onPressed: () {
setState(() {
turns += .125;
});
},
child: const Text('Tap me!')),
),
Flutter 允许您创建自定义隐式动画。要组合一个新的动画小部件,请使用 TweenAnimationBuilder
。
显式动画
对于显式动画,SwiftUI 使用 withAnimation()
函数。
Flutter 包含使用格式为 FooTransition
的显式动画小部件。一个示例是 RotationTransition
类。
Flutter 还允许您使用 AnimatedWidget
或 AnimatedBuilder
创建自定义显式动画。
要详细了解 Flutter 中的动画,请参阅 动画概述。
在屏幕上绘制
在 SwiftUI 中,您可以使用 CoreGraphics
在屏幕上绘制线条和形状。
Flutter 有一个基于 Canvas
类的 API,其中包含两个帮助您进行绘制的类
-
CustomPaint
,需要一个绘图器CustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ),
-
CustomPainter
,实现您的算法以绘制画布。class 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
// 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
。ListView.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()
提取参数class 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'),
);
},
child: const Text(
'Open website',
),
),
主题、样式和媒体
您可以毫不费力地设置Flutter应用的样式。设置样式包括在浅色和深色主题之间切换、更改文本和UI组件的设计等。本部分介绍如何设置应用的样式。
使用深色模式
在SwiftUI中,在View
上调用preferredColorScheme()
函数以使用深色模式。
在Flutter中,您可以在应用级别控制浅色和深色模式。要控制亮度模式,请使用App
类的theme
属性
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
部分下添加你的自定义字体。flutter: 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
文件中。flutter: assets: - images/Blueberries.jpg
添加图片后,使用 Image
小部件的 .asset()
构造函数来显示图片。此构造函数
- 使用提供的路径实例化给定的图片。
- 从与你的应用捆绑在一起的资产中读取图片。
- 在屏幕上显示图片。
要查看完整示例,请查看 Image
文档。
在应用中捆绑视频
在 SwiftUI 中,你可以通过两个步骤在你的应用中捆绑一个本地视频文件。首先,你导入 AVKit
框架,然后你实例化一个 VideoPlayer
视图。
在 Flutter 中,将 video_player 插件添加到你的项目中。此插件允许你从相同的代码库中创建可在 Android、iOS 和网络上运行的视频播放器。
- 将插件添加到你的应用中,并将视频文件添加到你的项目中。
- 将资源添加到你的
pubspec.yaml
文件中。 - 使用
VideoPlayerController
类加载并播放你的视频文件。
要查看完整的演练,请查看 video_player 示例。