面向 SwiftUI 開發者的 Flutter
學習如何將 SwiftUI 開發知識應用於構建 Flutter 應用。
想要使用 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 的外觀和工作方式。開發者將此類程式碼稱為宣告式框架。
檢視 (Views) vs. 元件 (Widgets)
#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<Widget>。mainAxisAlignment 屬性告訴 Flutter 如何定位具有額外空間的子元件。MainAxisAlignment.center 將子元件定位在主軸的中心。對於 Row,主軸是水平軸。
垂直對齊元件
#以下示例建立在上一節示例的基礎上。
在 SwiftUI 中,你使用 VStack 將元件排列成垂直柱狀。
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
Flutter 使用與上一個示例相同的 Dart 程式碼,只是將 Row 替換為 Column:
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有一個 index 引數,其範圍在 0 到 itemCount 減 1 之間。
前面的示例為每一項返回一個 ListTile 元件。ListTile 元件包含 height 和 font-size 等屬性。這些屬性有助於構建列表。但是,Flutter 允許你返回幾乎任何代表你的資料的元件。
顯示網格
#在 SwiftUI 中構建非條件網格時,你使用帶有 GridRow 的 Grid。
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 = <Widget>[
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(需要一個 painter)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; }
導航
#本節解釋如何在應用頁面之間導航、推送 (push) 和彈出 (pop) 機制等。
頁面間導航
#開發者使用不同的頁面構建 iOS 和 macOS 應用,這些頁面稱為導航路由 (navigation routes)。
在 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 the 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: () {},
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 示例。