面向 UIKit 開發者的 Flutter
有使用 UIKit 經驗的 iOS 開發者,如果想使用 Flutter 編寫移動應用,應查閱本指南。本指南解釋瞭如何將現有 UIKit 知識應用於 Flutter。
Flutter 是一個用於構建跨平臺應用的框架,它使用 Dart 程式語言。要了解 Dart 程式設計與 Swift 程式設計之間的一些差異,請查閱Swift 開發者學習 Dart和面向 Swift 開發者的 Flutter 併發。
您在構建 Flutter 應用時,您的 iOS 和 UIKit 知識和經驗非常有價值。Flutter 還在 iOS 上執行時對應用行為進行了一些調整。要了解如何調整,請參閱平臺適配。
將本指南作為一本菜譜。跳著查閱,找到能解決您最相關需求的問題。
概述
#作為介紹,請觀看以下影片。它概述了 Flutter 在 iOS 上的工作原理以及如何使用 Flutter 構建 iOS 應用。
檢視與元件
#在 UIKit 中,您在 UI 中建立的大部分內容都是使用檢視物件完成的,這些物件是 `UIView` 類的例項。它們可以作為其他 `UIView` 類的容器,構成您的佈局。
在 Flutter 中,大致相當於 `UIView` 的是 `Widget`。元件不完全等同於 iOS 檢視,但當您熟悉 Flutter 的工作原理時,您可以將它們視為“您宣告和構建 UI 的方式”。
然而,這些與 `UIView` 有一些不同。首先,元件的生命週期不同:它們是不可變的,並且只存在直到需要更改。每當元件或其狀態更改時,Flutter 框架都會建立一個新的元件例項樹。相比之下,UIKit 檢視在更改時不會重新建立,而是一個可變實體,它只繪製一次,並且在使用 `setNeedsDisplay()` 使其失效之前不會重新繪製。
此外,與 `UIView` 不同,Flutter 的元件是輕量級的,部分原因是它們的不可變性。因為它們本身不是檢視,也不是直接繪製任何東西,而只是 UI 及其語義的描述,這些描述會在底層“膨脹”成實際的檢視物件。
Flutter 包含 Material Components 庫。這些元件實現了 Material Design 指南。Material Design 是一個靈活的設計系統,針對所有平臺進行了最佳化,包括 iOS。
但 Flutter 足夠靈活和富有表現力,可以實現任何設計語言。在 iOS 上,您可以使用 Cupertino 元件庫來生成看起來像 Apple 的 iOS 設計語言的介面。
更新元件
#要在 UIKit 中更新檢視,您可以直接對其進行修改。在 Flutter 中,元件是不可變的,不能直接更新。相反,您必須操作元件的狀態。
這就是有狀態 (Stateful) 和無狀態 (Stateless) 元件概念的由來。`StatelessWidget` 顧名思義——一個沒有附加狀態的元件。
當您描述的使用者介面部分除了元件中的初始配置資訊之外不依賴任何東西時,`StatelessWidget` 很有用。
例如,對於 UIKit,這類似於放置一個 `UIImageView`,其 `image` 是您的徽標。如果徽標在執行時不更改,請在 Flutter 中使用 `StatelessWidget`。
如果您想根據發出 HTTP 呼叫後收到的資料動態更改 UI,請使用 `StatefulWidget`。HTTP 呼叫完成後,通知 Flutter 框架元件的 `State` 已更新,以便它可以更新 UI。
無狀態和有狀態元件之間的重要區別在於,`StatefulWidget` 具有一個 `State` 物件,該物件儲存狀態資料並將其跨樹重建傳遞,因此不會丟失。
如果您有疑問,請記住這條規則:如果一個元件在 `build` 方法之外發生變化(例如,由於執行時使用者互動),則它是有狀態的。如果元件一旦構建就永不更改,則它是無狀態的。但是,即使元件是有狀態的,如果其包含的父元件本身沒有對這些更改(或其他輸入)做出反應,則它仍然可以是無狀態的。
以下示例展示瞭如何使用 `StatelessWidget`。常見的 `StatelessWidget` 是 `Text` 元件。如果您檢視 `Text` 元件的實現,您會發現它繼承自 `StatelessWidget`。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);如果您檢視上面的程式碼,您可能會注意到 `Text` 元件沒有附帶任何顯式狀態。它渲染其建構函式中傳遞的內容,僅此而已。
但是,如果您想讓“我喜歡 Flutter”動態更改,例如在點選 `FloatingActionButton` 時怎麼辦?
要實現此目的,請將 `Text` 元件包裝在 `StatefulWidget` 中,並在使用者點選按鈕時更新它。
例如
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default placeholder text
String textToShow = 'I Like Flutter';
void _updateText() {
setState(() {
// Update the text
textToShow = 'Flutter is Awesome!';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}元件佈局
#在 UIKit 中,您可以使用 Storyboard 檔案來組織檢視和設定約束,或者您可以在檢視控制器中以程式設計方式設定約束。在 Flutter 中,透過組合元件樹在程式碼中宣告佈局。
以下示例展示瞭如何顯示一個帶填充的簡單 Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: CupertinoButton(
onPressed: () {},
padding: const EdgeInsets.only(left: 10, right: 10),
child: const Text('Hello'),
),
),
);
}您可以為任何元件新增填充,這模仿了 iOS 中約束的功能。
您可以在元件目錄中檢視 Flutter 提供的佈局。
移除元件
#在 UIKit 中,您在父檢視上呼叫 `addSubview()`,或在子檢視上呼叫 `removeFromSuperview()` 來動態新增或刪除子檢視。在 Flutter 中,由於元件是不可變的,因此沒有與 `addSubview()` 直接等效的功能。相反,您可以向父檢視傳遞一個返回元件的函式,並使用布林標誌控制該子檢視的建立。
以下示例展示瞭如何在使用者點選 `FloatingActionButton` 時在兩個元件之間切換
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text('Toggle One');
}
return CupertinoButton(onPressed: () {}, child: const Text('Toggle Two'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: _getToggleChild()),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}動畫
#在 UIKit 中,您透過在檢視上呼叫 `animate(withDuration:animations:)` 方法來建立動畫。在 Flutter 中,使用動畫庫將元件包裝在動畫元件中。
在 Flutter 中,使用 `AnimationController`,它是一個 `Animation
例如,您可以使用 `CurvedAnimation` 來實現沿著插值曲線的動畫。從這個意義上說,控制器是動畫進度的“主”源,而 `CurvedAnimation` 計算替換控制器預設線性運動的曲線。像元件一樣,Flutter 中的動畫也透過組合工作。
在構建元件樹時,您將 `Animation` 分配給元件的動畫屬性,例如 `FadeTransition` 的不透明度,並告訴控制器開始動畫。
以下示例展示瞭如何編寫一個 FadeTransition,當您按下 FloatingActionButton 時,該轉換會將 Widget 淡入為徽標
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Fade Demo',
home: MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
const MyFadeTest({super.key, required this.title});
final String title;
@override
State<MyFadeTest> createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: FadeTransition(
opacity: curve,
child: const FlutterLogo(size: 100),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward();
},
tooltip: 'Fade',
child: const Icon(Icons.brush),
),
);
}
}有關更多資訊,請參閱動畫與動作 Widgets、動畫教程和動畫概述。
螢幕繪製
#在 UIKit 中,您使用 `CoreGraphics` 在螢幕上繪製線條和形狀。Flutter 有一個基於 `Canvas` 類的不同 API,還有另外兩個類可以幫助您繪製:`CustomPaint` 和 `CustomPainter`,後者實現了您的演算法來繪製到畫布上。
要了解如何在 Flutter 中實現簽名畫家,請參閱 Collin 在 StackOverflow 上的回答。
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
const Signature({super.key});
@override
State<Signature> createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset?>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
Offset localPosition = referenceBox.globalToLocal(
details.globalPosition,
);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child: 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;
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;
}元件不透明度
#在 UIKit 中,所有內容都有 `.opacity` 或 `.alpha`。在 Flutter 中,大多數情況下,您需要將元件包裝在 `Opacity` 元件中才能實現此功能。
自定義元件
#在 UIKit 中,您通常透過子類化 `UIView` 或使用預先存在的檢視來重寫和實現方法以實現所需的行為。在 Flutter 中,透過組合更小的元件(而不是擴充套件它們)來構建自定義元件。
例如,如何構建一個在建構函式中接受標籤的 CustomButton?透過組合帶標籤的 ElevatedButton 來建立 CustomButton,而不是透過擴充套件 ElevatedButton
class CustomButton extends StatelessWidget {
const CustomButton(this.label, {super.key});
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}然後像使用任何其他 Flutter Widget 一樣使用 CustomButton
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}管理依賴項
#在 iOS 中,您透過將依賴項新增到 `Podfile` 中來使用 CocoaPods 新增依賴項。Flutter 使用 Dart 的構建系統和 Pub 包管理器來處理依賴項。這些工具將原生 Android 和 iOS 封裝應用的構建委託給各自的構建系統。
儘管 Flutter 專案的 iOS 資料夾中有一個 Podfile,但只有在您新增每個平臺整合所需的原生依賴項時才使用它。通常,使用 `pubspec.yaml` 在 Flutter 中宣告外部依賴項。一個很好的地方可以找到 Flutter 的優秀包是 pub.dev。
導航
#本文件的這一部分討論了應用頁面之間的導航、推拉機制等。
頁面導航
#在 UIKit 中,要在檢視控制器之間跳轉,您可以使用 `UINavigationController` 來管理要顯示的檢視控制器堆疊。
Flutter 也有類似的實現,使用 `Navigator` 和 `Routes`。`Route` 是應用“螢幕”或“頁面”的抽象,`Navigator` 是一個管理路由的元件。一個路由大致對映到一個 `UIViewController`。導航器的工作方式與 iOS `UINavigationController` 類似,它可以根據您是要導航到檢視還是從檢視返回來 `push()` 和 `pop()` 路由。
要在頁面之間導航,您有幾種選擇
- 指定路由名稱的 `Map`。
- 直接導航到路由。
以下示例構建一個 `Map`。
void main() {
runApp(
CupertinoApp(
home: const MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder>{
'/a': (context) => const MyPage(title: 'page A'),
'/b': (context) => const MyPage(title: 'page B'),
'/c': (context) => const MyPage(title: 'page C'),
},
),
);
}透過將其名稱 `push` 到 `Navigator` 來導航到路由。
Navigator.of(context).pushNamed('/b');`Navigator` 類處理 Flutter 中的路由,並用於從已推送到堆疊的路由獲取結果。這透過 `await` `push()` 返回的 `Future` 來完成。
例如,要啟動一個讓使用者選擇位置的 `location` 路由,您可以這樣做
Object? coordinates = await Navigator.of(context).pushNamed('/location');然後,在您的 `location` 路由中,一旦使用者選擇了他們的位置,使用結果 `pop()` 堆疊
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});導航到另一個應用
#在 UIKit 中,要將使用者傳送到另一個應用程式,您可以使用特定的 URL 方案。對於系統級應用程式,方案取決於應用程式。要在 Flutter 中實現此功能,請建立原生平臺整合,或使用現有外掛,例如`url_launcher`。
手動返回
#從 Dart 程式碼呼叫 `SystemNavigator.pop()` 會呼叫以下 iOS 程式碼
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
[((UINavigationController*)viewController) popViewControllerAnimated:NO];
}如果這不能滿足您的需求,您可以建立自己的平臺通道來呼叫任意 iOS 程式碼。
處理本地化
#與擁有 `Localizable.strings` 檔案的 iOS 不同,Flutter 目前沒有專門的系統來處理字串。目前,最佳實踐是將您的文字內容宣告為類中的靜態欄位,然後從那裡訪問它們。例如
class Strings {
static const String welcomeMessage = 'Welcome To Flutter';
}您可以像這樣訪問您的字串
Text(Strings.welcomeMessage);預設情況下,Flutter 僅支援美國英語字串。如果您需要新增對其他語言的支援,請包含 `flutter_localizations` 包。您可能還需要新增 Dart 的 `intl` 包以使用 i10n 機制,例如日期/時間格式。
dependencies:
flutter_localizations:
sdk: flutter
intl: any # Use version of intl from flutter_localizations.要使用 `flutter_localizations` 包,請在應用元件上指定 `localizationsDelegates` 和 `supportedLocales`
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
// Add app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: <Locale>[
Locale('en', 'US'), // English
Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
);
}
}委託包含實際的本地化值,而 `supportedLocales` 定義應用支援的區域設定。上面的示例使用 `MaterialApp`,因此它同時具有用於基本元件本地化值的 `GlobalWidgetsLocalizations` 和用於 Material 元件本地化的 `MaterialWidgetsLocalizations`。如果您的應用使用 `WidgetsApp`,則不需要後者。請注意,這兩個委託包含“預設”值,但如果您也希望這些值本地化,則需要為自己的應用的本地化副本提供一個或多個委託。
初始化後,`WidgetsApp`(或 `MaterialApp`)會為您建立一個`Localizations` 元件,並帶上您指定的委託。裝置的當前區域設定始終可以通過當前上下文中的 `Localizations` 元件(以 `Locale` 物件的形式)或使用`Window.locale` 訪問。
要訪問本地化資源,請使用 `Localizations.of()` 方法訪問給定委託提供的特定本地化類。使用 `intl_translation` 包將可翻譯的文字提取到 arb 檔案中進行翻譯,然後將其匯入回應用中以與 `intl` 一起使用。
有關 Flutter 中國際化和本地化的更多詳細資訊,請參閱國際化指南,其中包含使用和不使用 `intl` 包的示例程式碼。
檢視控制器
#本文件的這一部分討論了 Flutter 中 ViewController 的等效項以及如何監聽生命週期事件。
Flutter 中檢視控制器的等效項
#在 UIKit 中,`ViewController` 表示使用者介面的一部分,最常用於螢幕或部分。這些元件組合在一起以構建複雜的 UI,並幫助擴充套件您的應用程式 UI。在 Flutter 中,這項工作由 Widgets 完成。如導航部分所述,Flutter 中的螢幕由 Widgets 表示,因為“一切都是元件!” 使用 `Navigator` 在不同的表示不同螢幕或頁面,或者可能是相同資料的不同狀態或渲染的 `Route` 之間移動。
監聽生命週期事件
#在 UIKit 中,您可以覆蓋 `ViewController` 的方法來捕獲檢視本身的生命週期方法,或在 `AppDelegate` 中註冊生命週期回撥。在 Flutter 中,您沒有這些概念,但您可以透過掛鉤 `WidgetsBinding` 觀察器並監聽 `didChangeAppLifecycleState()` 更改事件來監聽生命週期事件。
可觀察的生命週期事件有
不活動- 應用程式處於非活動狀態,不接收使用者輸入。此事件僅在 iOS 上有效,因為 Android 上沒有等效事件。
已暫停- 應用程式當前對使用者不可見,不響應使用者輸入,但正在後臺執行。
已恢復- 應用程式可見並響應使用者輸入。
正在掛起- 應用程式暫時掛起。iOS 平臺沒有等效事件。
有關這些狀態含義的更多詳細資訊,請參閱`AppLifecycleState` 文件。
佈局
#本節討論 Flutter 中的不同佈局以及它們與 UIKit 的比較。
顯示列表檢視
#在 UIKit 中,您可以在 `UITableView` 或 `UICollectionView` 中顯示列表。在 Flutter 中,您可以使用 `ListView` 實現類似的功能。在 UIKit 中,這些檢視具有委託方法,用於決定行數、每個索引路徑的單元格以及單元格的大小。
由於 Flutter 的不可變元件模式,您將元件列表傳遞給 `ListView`,Flutter 會負責確保滾動快速流暢。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
final List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}檢測點選事件
#在 UIKit 中,您實現委託方法 `tableView:didSelectRowAtIndexPath:`。在 Flutter 中,使用傳入元件提供的觸控處理。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}動態更新 ListView
#在 UIKit 中,您更新列表檢視的資料,並使用 `reloadData` 方法通知表格或集合檢視。
在 Flutter 中,如果您在 `setState()` 中更新元件列表,您會很快發現數據在視覺上沒有變化。這是因為當呼叫 `setState()` 時,Flutter 渲染引擎會檢查元件樹以檢視是否有任何更改。當它到達您的 `ListView` 時,它會執行 `==` 檢查,並確定兩個 `ListView` 是相同的。沒有任何變化,因此不需要更新。
為了簡單地更新您的 `ListView`,在 `setState()` 內部建立一個新的 `List`,並將資料從舊列表複製到新列表。雖然這種方法很簡單,但不建議用於大型資料集,如以下示例所示。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = <Widget>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: widgets),
);
}
}推薦、高效且有效的構建列表的方法是使用 `ListView.Builder`。當您有一個動態列表或資料量非常大的列表時,此方法非常有用。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
}與其建立 `ListView`,不如建立一個 `ListView.builder`,它需要兩個關鍵引數:列表的初始長度和 `ItemBuilder` 函式。
`ItemBuilder` 函式類似於 iOS 表格或集合檢視中的 `cellForItemAt` 委託方法,因為它接受一個位置,並返回您希望在該位置渲染的單元格。
最後,但最重要的是,請注意 `onTap()` 函式不再重新建立列表,而是 `add` 到列表中。
建立滾動檢視
#在 UIKit 中,您將檢視包裝在 `ScrollView` 中,以便使用者在需要時可以滾動內容。
在 Flutter 中,最簡單的方法是使用 `ListView` 元件。這既充當 `ScrollView` 又充當 iOS `TableView`,因為您可以以垂直格式佈局元件。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}有關如何在 Flutter 中佈局元件的更詳細文件,請參閱佈局教程。
手勢檢測和觸控事件處理
#本節討論如何在 Flutter 中檢測手勢和處理不同的事件,以及它們與 UIKit 的比較。
新增點選監聽器
#在 UIKit 中,您將 `GestureRecognizer` 附加到檢視以處理點選事件。在 Flutter 中,有兩種新增觸控監聽器的方法
- 如果元件支援事件檢測,則向其傳遞一個函式,並在函式中處理事件。例如,`ElevatedButton` 元件有一個 `onPressed` 引數
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}- 如果元件不支援事件檢測,請將元件包裝在 GestureDetector 中,並將一個函式傳遞給 `onTap` 引數。
class SampleTapApp extends StatelessWidget {
const SampleTapApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
developer.log('tap');
},
child: const FlutterLogo(size: 200),
),
),
);
}
}處理其他手勢
#使用 `GestureDetector`,您可以監聽各種手勢,例如
點選
onTapDown- 可能導致點選的指標已在特定位置接觸螢幕。
onTapUp- 觸發點選的指標已在特定位置停止接觸螢幕。
onTap- 發生了點選。
onTapCancel- 之前觸發 `onTapDown` 的指標不會導致點選。
雙擊
onDoubleTap- 使用者在同一位置快速連續點選螢幕兩次。
長按
onLongPress- 指標在同一位置長時間保持與螢幕接觸。
垂直拖動
onVerticalDragStart- 指標已接觸螢幕並可能開始垂直移動。
onVerticalDragUpdate- 與螢幕接觸的指標在垂直方向上移動得更遠。
onVerticalDragEnd- 之前與螢幕接觸並垂直移動的指標不再與螢幕接觸,並且在停止接觸螢幕時以特定速度移動。
水平拖動
onHorizontalDragStart- 指標已接觸螢幕並可能開始水平移動。
onHorizontalDragUpdate- 與螢幕接觸的指標在水平方向上移動得更遠。
onHorizontalDragEnd- 之前與螢幕接觸並水平移動的指標不再與螢幕接觸。
以下示例展示了一個 GestureDetector,它在雙擊時旋轉 Flutter 徽標
class SampleApp extends StatefulWidget {
const SampleApp({super.key});
@override
State<SampleApp> createState() => _SampleAppState();
}
class _SampleAppState extends State<SampleApp>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
child: RotationTransition(
turns: curve,
child: const FlutterLogo(size: 200),
),
),
),
);
}
}主題、樣式和媒體
#Flutter 應用程式易於樣式化;您可以在淺色和深色主題之間切換,更改文字和 UI 元件的樣式等。本節涵蓋了 Flutter 應用程式樣式化的各個方面,並比較了您在 UIKit 中可能如何做同樣的事情。
使用主題
#Flutter 開箱即用,提供了 Material Design 的精美實現,它負責處理您通常需要的大量樣式和主題需求。
要在您的應用程式中充分利用 Material Components,請宣告一個頂級元件 `MaterialApp` 作為您應用程式的入口點。`MaterialApp` 是一個方便的元件,它封裝了 Material Design 應用程式通常需要的一些元件。它透過新增 Material 特定的功能來擴充套件 `WidgetsApp`。
但 Flutter 足夠靈活和富有表現力,可以實現任何設計語言。在 iOS 上,您可以使用 Cupertino 庫來生成符合 人機介面指南的介面。有關這些元件的完整集合,請參閱 Cupertino 元件庫。
您還可以使用 WidgetsApp 作為您的應用 Widget,它提供了一些相同的功能,但不如 MaterialApp 豐富。
要自定義任何子元件的顏色和樣式,請將 `ThemeData` 物件傳遞給 `MaterialApp` 元件。例如,在下面的程式碼中,種子顏色方案設定為深紫色,分隔線顏色設定為灰色。
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
dividerColor: Colors.grey,
),
home: const SampleAppPage(),
);
}
}使用自定義字型
#在 UIKit 中,您將任何 `ttf` 字型檔案匯入到專案中並在 `info.plist` 檔案中建立引用。在 Flutter 中,將字型檔案放在資料夾中並在 `pubspec.yaml` 檔案中引用它,類似於匯入影像的方式。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic然後將字型分配給您的 Text Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: const Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}文字樣式
#除了字型之外,您還可以在 Text Widget 上自定義其他樣式元素。Text Widget 的 style 引數接受一個 TextStyle 物件,您可以在其中自定義許多引數,例如
colordecorationdecorationColordecorationStylefontFamilyfontSizefontStylefontWeighthashCodeheightinheritletterSpacingtextBaselinewordSpacing
在應用中捆綁影像
#雖然 iOS 將影像和資產視為不同的項,但 Flutter 應用程式只有資產。iOS 上放置在 `Images.xcasset` 資料夾中的資源,在 Flutter 中放置在資產資料夾中。與 iOS 一樣,資產可以是任何型別的檔案,而不僅僅是影像。例如,您可能在 `my-assets` 資料夾中有一個 JSON 檔案
my-assets/data.json在 `pubspec.yaml` 檔案中宣告資產
assets:
- my-assets/data.json然後使用 `AssetBundle` 從程式碼中訪問它
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}對於影像,Flutter 遵循 iOS 類似的簡單基於密度的格式。影像資產可以是 `1.0x`、`2.0x`、`3.0x` 或任何其他乘數。Flutter 的 `devicePixelRatio` 表示單個邏輯畫素中的物理畫素比。
資產可以放置在任何任意資料夾中——Flutter 沒有預定義的資料夾結構。您在 `pubspec.yaml` 檔案中宣告資產(及其位置),Flutter 會載入它們。
例如,要將名為 `my_icon.png` 的影像新增到您的 Flutter 專案中,您可以決定將其儲存在任意名為 `images` 的資料夾中。將基本影像(1.0x)放在 `images` 資料夾中,並將其他變體放在以相應比例乘數命名的子資料夾中
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image接下來,在 `pubspec.yaml` 檔案中宣告這些影像
assets:
- images/my_icon.png您現在可以使用 `AssetImage` 訪問您的影像
image: AssetImage('images/a_dot_burr.png'),或直接在 `Image` 元件中
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}有關更多詳細資訊,請參閱在 Flutter 中新增資產和影像。
表單輸入
#本節討論如何在 Flutter 中使用表單以及它們與 UIKit 的比較。
獲取使用者輸入
#鑑於 Flutter 使用帶有獨立狀態的不可變元件,您可能想知道使用者輸入如何融入其中。在 UIKit 中,您通常會在需要提交使用者輸入或對其進行操作時查詢元件的當前值。這在 Flutter 中是如何工作的?
實際上,表單與 Flutter 中的所有內容一樣,都由專用元件處理。如果您有 `TextField` 或 `TextFormField`,您可以提供一個 `TextEditingController` 來檢索使用者輸入
class _MyFormState extends State<MyForm> {
// Create a text controller and use it to retrieve the current value.
// of the TextField!
final myController = TextEditingController();
@override
void dispose() {
// Clean up the controller when disposing of the Widget.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Retrieve Text Input')),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(controller: myController),
),
floatingActionButton: FloatingActionButton(
// When the user presses the button, show an alert dialog with the
// text the user has typed into our text field.
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the user has typed in using our
// TextEditingController.
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: const Icon(Icons.text_fields),
),
);
}
}您可以在檢索文字欄位的值中找到更多資訊和完整的程式碼列表。
文字欄位中的佔位符
#在 Flutter 中,您可以透過為 `Text` 元件的 decoration 建構函式引數新增 `InputDecoration` 物件,輕鬆地為您的欄位顯示“提示”或佔位符文字
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)顯示驗證錯誤
#就像您使用“提示”一樣,將 `InputDecoration` 物件傳遞給 `Text` 元件的 decoration 建構函式。
但是,您不希望一開始就顯示錯誤。相反,當用戶輸入無效資料時,更新狀態,並傳遞一個新的 InputDecoration 物件。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String? _errorText;
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: TextField(
onSubmitted: (text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(
hintText: 'This is a hint',
errorText: _errorText,
),
),
),
);
}
}執行緒與非同步
#本節討論 Flutter 中的併發性以及它與 UIKit 的比較。
編寫非同步程式碼
#Dart 具有單執行緒執行模型,支援 `Isolate`(在另一個執行緒上執行 Dart 程式碼的方式)、事件迴圈和非同步程式設計。除非您生成一個 `Isolate`,否則您的 Dart 程式碼會在主 UI 執行緒中執行,並由事件迴圈驅動。Flutter 的事件迴圈相當於 iOS 主迴圈——即,附加到主執行緒的 `Looper`。
Dart 的單執行緒模型並不意味著您必須將所有內容都作為阻塞操作執行,導致 UI 凍結。相反,使用 Dart 語言提供的非同步功能,例如 `async` / `await`,來執行非同步工作。
例如,您可以使用 async/await 執行網路程式碼,而不會導致 UI 卡頓,讓 Dart 完成繁重的工作
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}一旦 `await` 的網路呼叫完成,透過呼叫 `setState()` 更新 UI,這將觸發元件子樹的重建並更新資料。
以下示例非同步載入資料並將其顯示在 ListView 中
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Widget getRow(int index) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text('Row ${data[index]['title']}'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
),
);
}
}有關在後臺執行工作的更多資訊,以及 Flutter 與 iOS 的區別,請參閱下一節。
移至後臺執行緒
#由於 Flutter 是單執行緒並執行事件迴圈(類似於 Node.js),因此您不必擔心執行緒管理或生成後臺執行緒。如果您正在進行 I/O 密集型工作,例如磁碟訪問或網路呼叫,那麼您可以安全地使用 `async`/`await`,然後就完成了。另一方面,如果您需要進行計算密集型工作,使 CPU 保持忙碌,您需要將其移動到 `Isolate` 以避免阻塞事件迴圈。
對於 I/O 密集型工作,將函式宣告為 async 函式,並在函式內部 await 長時間執行的任務
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}您通常就是這樣進行網路或資料庫呼叫,它們都是 I/O 操作。
但是,有時您可能會處理大量資料並且 UI 掛起。在 Flutter 中,使用 Isolate 來利用多個 CPU 核心來執行長時間執行或計算密集型任務。
Isolate 是獨立的執行執行緒,不與主執行記憶體堆共享任何記憶體。這意味著您無法從主執行緒訪問變數,也無法透過呼叫 `setState()` 更新 UI。Isolate 名副其實,無法共享記憶體(例如,以靜態欄位的形式)。
以下示例以一個簡單的 isolate 形式展示瞭如何將資料共享回主執行緒以更新 UI。
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}這裡,`dataLoader()` 是在自己的獨立執行執行緒中執行的 `Isolate`。在 Isolate 中,您可以執行更 CPU 密集型的處理(例如,解析一個大型 JSON),或執行計算密集型數學運算,例如加密或訊號處理。
您可以在下面執行完整示例
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Widget getBody() {
bool showLoadingDialog = data.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
}發起網路請求
#當您使用流行的 `http` 包時,在 Flutter 中進行網路呼叫變得很容易。這抽象了您通常會自己實現的許多網路功能,從而簡化了網路呼叫。
要將 http 包新增為依賴項,請執行 flutter pub add。
flutter pub add http要進行網路呼叫,請對 `async` 函式 `http.get()` 呼叫 `await`
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}顯示長時間執行任務的進度
#在 UIKit 中,您通常會在後臺執行長時間執行的任務時使用 `UIProgressView`。
在 Flutter 中,使用 ProgressIndicator Widget。透過布林標誌控制何時渲染它來以程式設計方式顯示進度。在長時間執行的任務開始之前通知 Flutter 更新其狀態,並在任務結束後將其隱藏。
在下面的示例中,構建函式分為三個不同的函式。如果 `showLoadingDialog` 為 `true`(當 `widgets.length == 0` 時),則渲染 `ProgressIndicator`。否則,渲染 `ListView` 以及網路呼叫返回的資料。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}
Widget getBody() {
if (showLoadingDialog) {
return getProgressDialog();
}
return getListView();
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
}