SwiftUI 工程师的 Flutter 指南
本文翻译自 Flutter for SwiftUI Developers | Flutter
介绍
- 本文为
SwiftUI
工程师入门 Flutter专用。介绍了如何将SwiftUI 经验应用于 Flutter 中。 - Flutter 是一个用于构建
跨平台App
的框架,它使用Dart
语言。
概述
Flutter 和 SwiftUI 使用纯代码描述 UI 的外观和工作方式。这种代码被称为声明式UI
。
View 与 Widget
SwiftUI 将 UI 组件表示为View
。
Text("Hello, World!") // 这是一个 View
.padding(10) // View 的属性
Flutter 将 UI 组件表示为Widget
。
Padding( // 这是一个 Widget
padding: EdgeInsets.all(10.0), // Widget 的属性
child: Text("Hello, World!"), // 子 Widget
)));
SwiftUI 嵌套视图,而 Flutter 嵌套 Widget, 层层嵌套形成树形
结构。
布局过程
SwiftUI 布局过程
- 第一阶段 —— 讨价还价
- 父View为子View提供建议尺寸
- 子view根据自身的特性,返回一个size
- 父view根据子view返回的size为其进行布局
- 第二阶段 —— 布局到屏幕上
父View根据布局系统提供的屏幕区域
为子视图设置渲染的位置和尺寸
。此时,视图树上的每个View都将与屏幕上的具体位置联系起来。
Flutter 布局过程
- 父节点向子节点传递约束信息,限制
子节点的最大和最小宽高
- 子节点根据
自己的约束
信息来确定自己的大小
(Szie)。 - 父节点根据特定的规则(不同的组件会有不同的布局算法)确定每一个子节点在父节点空间中的位置,用偏移
offset
表示。 递归
整个过程,确定每一个节点的位置和大小。
可以看到,组件的大小是由自身
来决定的,而组件的位置是由父组件来决定的
。
UI基础知识
下面介绍 UI 开发的基础知识,并将Flutter和SwiftUI进行对比。
SwiftUI启动App
@main
struct MyApp: App { // App 对象
var body: some Scene {
WindowGroup {
HomePage()
}
}
}
struct HomePage: View { // 显示首页
var body: some View {
Text("Hello, World!")
}
}
Flutter启动App
void main() {
runApp(const MyApp()); // App对象
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// CupertinoApp,使用iOS风格的控件
return const CupertinoApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text(
'Hello, World!',
),
),
);
}
}
默认情况下,SwiftUI 的View
默认居中
。 Flutter使用Center
组件包装让文本居中。
添加按钮
SwiftUI使用Button创建按钮。
Button("Do something") {
// 按钮点击回调
}
为了在 Flutter 中达到相同的结果, 使用类:CupertinoButton
CupertinoButton(
onPressed: () {
// 按钮点击回调
},
child: const Text('Do something'),
)
水平对齐组件
SwiftUI
HStack
创建水平Stack视图VStack
创建垂直Stack视图
HStack { // 添加图像和文本到HStack中
Image(systemName: "globe")
Text("Hello, world!")
}
Flutter
Row( // 使用 Row 创建
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.globe),
Text('Hello, world!'),
],
),
垂直对齐组件
SwiftUI
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
Flutter
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.globe),
Text('Hello, world!'),
],
),
列表视图(List View)
SwiftUI
struct Person: Identifiable { // Identifiable 唯一标识model
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 { // List 表示一组项目
ForEach(persons) { person in
Text(person.name)
}
}
}
}
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( // build函数构造ListView
itemCount: items.length, // 子项目的数量
itemBuilder: (context, index) { // 每一项的内容
return ListTile(
title: Text(items[index].name),
);
},
),
);
}
}
Grid 视图
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 Widget。
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( // GridView构造器
// 设置GridView参数
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 每行显示数量
mainAxisExtent: 40.0, // item高度
),
itemCount: widgets.length, // item总数量
itemBuilder: (context, index) => widgets[index],
),
);
}
}
创建 Scroll View
SwiftUI
ScrollView { // 定义滚动视图
VStack(alignment: .leading) {
ForEach(persons) { person in // 子视图
PersonView(person: person)
}
}
}
Flutter 使用 SingleChildScrollView。
SingleChildScrollView(
child: Column(
children: mockPersons
.map(
(person) => PersonView(
person: person,
),
)
.toList(),
),
),
管理状态
SwiftUI,使用属性包装器@State
来表示View的内部状态。
struct ContentView: View {
@State private var counter = 0;
var body: some View {
VStack{
Button("+") { counter+=1 } // @State属性改变, Text会自动刷新
Text(String(counter))
}
}}
Flutter 使用 StatefulWidget
和State
管理状态。
State存储Widget的状态。 要更改Widget
的状态,使用setState()
告诉Flutter重绘Widget
。
以下示例显示了计数器应用的一部分:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState(); // 创建State
}
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(() { // setState通知Text刷新
_counter++;
}),
child: const Text('+'),
),
],
),
),
);
}
}
动画
存在如下两种类型的 UI 动画。
- 隐式动画
SwiftUI
Button(“Tap me!”){
angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1)) // 使用animation函数处理隐式动画。
Flutter 有专门处理隐式动画的Widget(Animated+XXX)。
AnimatedRotation( // 旋转动画
duration: const Duration(seconds: 1),
turns: turns,
curve: Curves.easeIn,
child: TextButton(
onPressed: () {
setState(() {
turns += .125;
});
},
child: const Text('Tap me!')),
),
-
显式动画
- SwiftUI 使用
withAnimation()
函数 - Flutter 使用专门的Widget如RotationTransition(
XXX+Transition
)
- SwiftUI 使用
在屏幕上绘图
- SwiftUI使用
CoreGraphics
框架绘制线条和形状 - Flutter使用
CustomPaint
和CustomPainter
进行绘制
CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
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.0;
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;
}
Navigation(导航)
本部分介绍如何在App的页面之间navigate(导航)
、push
和pop
。
开发人员使用称为navigation routes
在不同页面跳转
在 SwiftUI 中,使用NavigationStack
表示页面栈
下面的示例创建一个显示人员列表的应用。点击那个人在新的NavigationLink
中显示人员的详细信息。
NavigationStack(path: $path) {
List {
ForEach(persons) { person in
NavigationLink(
person.name,
value: person
)
}
}
.navigationDestination(for: Person.self) { person in
PersonView(person: person)
}
}
Flutter的命名路由
// 定义 route name 为常量,方便复用
const detailsPageRouteName = '/details';
class App extends StatelessWidget {
const App({
super.key,
});
@override
Widget build(BuildContext context) {
return CupertinoApp(
home: const HomePage(),
// routes属性定义可用的命名路由和跳转的目标Widget
routes: {
detailsPageRouteName: (context) => const DetailsPage(),
},
);
}
}
下面的示例: 点击某人会push到此人的详细信息页面, 使用 Navigator pushNamed()
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: () { // 为ListTitle添加点击事件
// 将detailsPageRouteName命名路由push给Navigator,并将参数person传递给route。
Navigator.of(context).pushNamed(
detailsPageRouteName,
arguments: person,
);
},
);
},
),
class DetailsPage extends StatelessWidget {
const DetailsPage({super.key});
@override
Widget build(BuildContext context) {
final Person person = ModalRoute.of( // ModalRoute.of 读取参数
context,
)?.settings.arguments as Person;
// 读取person的年龄属性
final age = '${person.age} years old';
return Scaffold(
// 显示名字和年龄
body: Column(children: [Text(person.name), Text(age)]),
);
}
}
要创建更高级的导航和路由要求, 使用route package
,例如go_router。
手动pop返回
在 SwiftUI 中,使用dismiss
方法返回上一个界面
Button("Pop back") {
dismiss()
}
在 Flutter 中,使用Navigator的pop()
函数
TextButton(
onPressed: () {
// pop back到它的presenter
Navigator.of(context).pop();
},
child: const Text('Pop back'),
),
导航到其他App
在 SwiftUI 中,使用环境变量@Environment
打开一个指向其他App的URL。
@Environment(\.openURL) private var openUrl
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 App的样式。
样式包括:
- 在浅色和深色主题之间切换,
- 更改文本和 UI 组件的设计。
使用深色模式
- 在 SwiftUI 中,在View上调用函数
preferredColorScheme()
以使用深色模式 - 在 Flutter 中,在App级别控制明暗模式。
CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark, // 深色模式
),
home: HomePage(),
);
设置文本样式
在 SwiftUI 中, 使用font()
函数更改Text的字体
Text("Hello, world!")
.font(.system(size: 30, weight: .heavy))
.foregroundColor(.yellow)
在 Flutter 中,使用TextStyle
设置文本样式
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 中,设置child的样式,或修改按钮本身的属性。
child: CupertinoButton(
color: CupertinoColors.systemYellow, // 设置按钮的背景色
onPressed: () {},
padding: const EdgeInsets.all(16),
child: const Text( // 设置它的child节点Text的样式来修改按钮的文本样式
'Do something',
style: TextStyle(
color: CupertinoColors.systemBlue,
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
使用自定义字体
SwiftUI
Text("Hello")
.font(
Font.custom( // 使用自定义字体"BungeeSpice-Regular"
"BungeeSpice-Regular",
size: 40
)
)
在 Flutter 中,使用pubspec.yaml文件定义App资源, 此文件是跨平台的。添加字体包含如下步骤:
- 创建fonts文件夹来组织字体。
- 添加.ttf .otf .ttc样式文件到文件夹。
- 打开pubspec.yaml
- 在flutter的fonts结构下添加自定义字体
flutter:
fonts:
- family: BungeeSpice
fonts:
- asset: fonts/BungeeSpice-Regular.ttf
Text(
'Cupertino',
style: TextStyle(
fontSize: 40,
fontFamily: 'BungeeSpice', // 使用刚添加的BungeeSpice字体
),
)
App显示图像
-
在 SwiftUI 中,首先将image文件添加到Assets.xcassets中。 然后使用Image显示图像
-
在 Flutter 中添加图像,和添加自定义字体类似。
- 添加images文件夹到根目录。
- 添加asset到pubspec.yaml中
flutter:
assets:
- images/Blueberries.jpg
添加图像后,使用Image.asset()显示它。
在App播放视频
- SwiftUI: 使用 AVKitVideoPlayer
- Flutter: 使用 video_player插件
转载自:https://juejin.cn/post/7237487578864353339