Flutter 入门指南 | SwiftUI 开发者
希望使用 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 的外观和工作原理。开发人员将这种类型的代码称为声明式框架。
视图与 Widget
#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 中约束的工作原理,请参阅 了解约束。
设计系统
#由于 Flutter 面向多个平台,因此您的应用无需符合任何设计系统。虽然本指南以 Material 小部件为特色,但您的 Flutter 应用可以使用许多不同的设计系统
- 自定义 Material 小部件
- 社区构建的小部件
- 您自己的自定义小部件
- 遵循 Apple 人机界面指南的 Cupertino 小部件
Flutter 的 cupertino 库,适用于 iOS 开发人员
如果您正在寻找一个具有自定义设计系统的优秀参考应用,请查看 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
小部件有一个构建器方法。这类似于 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()
生成人员列表。点击一个人会将该人的详细信息页面推送到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, ); }, ); }, ),
定义
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 示例。
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-10-17。 查看源代码 或 报告问题。