適用於 UIKit 開發者的 Flutter
對於有使用 UIKit 經驗且想用 Flutter 編寫行動應用程式的 iOS 開發人員,應查看本指南。它解釋了如何將現有的 UIKit 知識應用於 Flutter。
Flutter 是一個用於建構跨平台應用程式的框架,它使用 Dart 程式語言。若要了解使用 Dart 程式設計與使用 Swift 程式設計之間的一些差異,請查看身為 Swift 開發人員學習 Dart和適用於 Swift 開發人員的 Flutter 並行處理。
當您使用 Flutter 建構應用程式時,您的 iOS 和 UIKit 知識與經驗非常寶貴。當在 iOS 上執行時,Flutter 也會對應用程式行為進行一些調整。若要了解其原理,請參閱平台調整。
將本指南當作食譜使用。跳到您最需要的相關問題。
總覽
#作為簡介,請觀看下列影片。它概述了 Flutter 在 iOS 上運作的方式,以及如何使用 Flutter 建構 iOS 應用程式。
Views (視圖) vs. Widgets (元件)
#在 UIKit 中,您在 UI 中建立的大部分內容都是使用視圖物件完成的,這些物件是 UIView
類別的實例。這些物件可以作為其他 UIView
類別的容器,構成您的佈局。
在 Flutter 中,與 UIView
大致對應的是 Widget
。元件並非與 iOS 視圖完全對應,但當您熟悉 Flutter 的運作方式時,您可以將它們視為「您宣告和建構 UI 的方式」。
然而,這些與 UIView
有一些差異。首先,元件的生命週期不同:它們是不可變的,只存在到需要變更為止。每當元件或其狀態變更時,Flutter 的框架就會建立新的元件實例樹狀結構。相較之下,UIKit 視圖在變更時不會重新建立,而是可變的實體,會繪製一次,直到使用 setNeedsDisplay()
使其失效才會重新繪製。
此外,與 UIView
不同,Flutter 的元件是輕量的,部分原因在於其不可變性。因為它們本身不是視圖,也不是直接繪製任何東西,而是 UI 的描述及其語意,會在底層「膨脹」成實際的視圖物件。
Flutter 包含 Material 元件程式庫。這些是實作Material Design 指南的元件。Material Design 是一種彈性的設計系統,針對包括 iOS 在內的所有平台最佳化。
但是 Flutter 具有足夠的彈性和表現力來實作任何設計語言。在 iOS 上,您可以使用Cupertino 元件程式庫來產生看起來像Apple iOS 設計語言的介面。
更新元件
#若要在 UIKit 中更新視圖,您會直接變更它們。在 Flutter 中,元件是不可變的,不會直接更新。相反地,您必須操控元件的狀態。
這就是 Stateful (有狀態) 與 Stateless (無狀態) 元件概念的來源。StatelessWidget
就如同它聽起來一樣,是沒有附加任何狀態的元件。
當您描述的使用者介面部分不依賴於元件中初始設定資訊以外的任何其他內容時,StatelessWidgets
非常有用。
例如,在 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 中,透過組成元件樹狀結構在程式碼中宣告您的佈局。
下列範例說明如何顯示一個包含邊距的簡單元件
@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<double>
,可以暫停、搜尋、停止和反轉動畫。它需要一個 Ticker
,在發生垂直同步時發出訊號,並在執行時於每個影格產生 0 和 1 之間的線性內插。然後,您建立一個或多個 Animation
並將它們附加到控制器。
例如,您可以使用 CurvedAnimation
沿著內插曲線實作動畫。從這個意義上說,控制器是動畫進度的「主要」來源,而 CurvedAnimation
計算取代控制器預設線性運動的曲線。與元件一樣,Flutter 中的動畫也透過組成來運作。
當建構元件樹狀結構時,您會將 Animation
指派給元件的動畫屬性,例如 FadeTransition
的不透明度,並告知控制器開始動畫。
下列範例說明如何撰寫 FadeTransition
,當您按下 FloatingActionButton
時,將元件淡入標誌
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),
),
);
}
}
如需更多資訊,請參閱動畫與動態元件、動畫教學課程和動畫總覽。
在螢幕上繪圖
#在 UIKit 中,您使用 CoreGraphics
在螢幕上繪製線條和形狀。Flutter 具有不同的 API,基於 Canvas
類別,另有兩個類別可協助您繪圖: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),
);
}
}
然後使用 CustomButton
,就像您使用任何其他 Flutter 元件一樣
@override
Widget build(BuildContext context) {
return const Center(
child: CustomButton('Hello'),
);
}
管理相依性
#在 iOS 中,您可以使用 CocoaPods 透過新增至您的 Podfile
來新增相依性。Flutter 使用 Dart 的建構系統和 Pub 套件管理員來處理相依性。這些工具會將原生 Android 和 iOS 包裝應用程式的建構委派給各自的建構系統。
雖然您的 Flutter 專案中的 iOS 資料夾中存在 Podfile,但只有在新增每個平台整合所需的原生相依性時才使用此選項。一般而言,請使用 pubspec.yaml
在 Flutter 中宣告外部相依性。在 pub.dev 上找到適用於 Flutter 的絕佳套件是個不錯的選擇。
導覽
#本文件的此章節討論應用程式頁面之間的導覽、推播和彈出機制等等。
在頁面之間導覽
#在 UIKit 中,若要瀏覽不同的視圖控制器,可以使用 UINavigationController
來管理要顯示的視圖控制器堆疊。
Flutter 有類似的實作方式,使用 Navigator
和 Routes
。Route
是應用程式「畫面」或「頁面」的抽象概念,而 Navigator
則是一個 widget,用於管理路由。一個路由大致對應到一個 UIViewController
。導覽器(navigator)的工作方式與 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 程式碼。
處理本地化
#與 iOS 擁有 Localizable.strings
檔案不同,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
套件,請在應用程式 widget 上指定 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
,因此它同時具有用於基本 widget 本地化值的 GlobalWidgetsLocalizations
和用於 Material widget 本地化的 MaterialWidgetsLocalizations
。如果您為應用程式使用 WidgetsApp
,則不需要後者。請注意,這兩個委派都包含「預設」值,但如果您希望也本地化您自己的應用程式可本地化的副本,則需要為其提供一個或多個委派。
初始化後,WidgetsApp
(或 MaterialApp
)會為您建立一個 Localizations
widget,其中包含您指定的委派。裝置的目前地區設定始終可以從目前上下文的 Localizations
widget(以 Locale
物件的形式)存取,或使用 Window.locale
存取。
若要存取本地化資源,請使用 Localizations.of()
方法來存取給定委派提供的特定本地化類別。使用 intl_translation
套件來將可翻譯的副本擷取到 arb 檔案中進行翻譯,並將其匯回應用程式以與 intl
一起使用。
如需 Flutter 中國際化和本地化的詳細資訊,請參閱國際化指南,其中包含使用和不使用 intl
套件的範例程式碼。
ViewController
#本節文件討論 Flutter 中 ViewController 的等效物,以及如何監聽生命週期事件。
Flutter 中與 ViewController 相對應的概念
#在 UIKit 中,ViewController
代表使用者介面的一部分,最常使用於畫面或區段。這些組合在一起以建立複雜的使用者介面,並幫助擴展應用程式的 UI。在 Flutter 中,這項工作由 Widgets 負責。如「導覽」章節中所述,Flutter 中的畫面由 Widgets 表示,因為「一切皆是 widget!」使用 Navigator
在不同的 Route
之間移動,這些 Route
代表不同的畫面或頁面,或者可能是相同資料的不同狀態或呈現方式。
監聽生命週期事件
#在 UIKit 中,您可以覆寫 ViewController
的方法來擷取視圖本身的生命週期方法,或在 AppDelegate
中註冊生命週期回呼。在 Flutter 中,您沒有這兩種概念,但是您可以透過掛鉤到 WidgetsBinding
觀察器並監聽 didChangeAppLifecycleState()
變更事件來監聽生命週期事件。
可觀察的生命週期事件為:
非活動中(inactive)
- 應用程式處於非活動狀態,並且未接收使用者輸入。此事件僅適用於 iOS,因為 Android 上沒有對應的事件。
已暫停(paused)
- 應用程式目前對使用者不可見,不回應使用者輸入,但在背景中執行。
已恢復(resumed)
- 應用程式可見且正在回應使用者輸入。
暫停中(suspending)
- 應用程式暫時暫停。iOS 平台沒有對應的事件。
如需這些狀態含義的詳細資訊,請參閱 AppLifecycleState
文件。
佈局
#本節討論 Flutter 中不同的版面配置,以及它們與 UIKit 的比較。
顯示列表視圖
#在 UIKit 中,您可能會在 UITableView
或 UICollectionView
中顯示清單。在 Flutter 中,您可以使用 ListView
進行類似的實作。在 UIKit 中,這些視圖具有委派方法,用於決定列數、每個索引路徑的儲存格以及儲存格的大小。
由於 Flutter 的不可變 widget 模式,您可以將 widget 清單傳遞給 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 中,請使用傳入的 widget 提供的觸控處理。
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()
中更新 widget 清單,您會很快發現您的資料在視覺上沒有變化。這是因為在呼叫 setState()
時,Flutter 渲染引擎會查看 widget 樹狀結構,以查看是否有任何變更。當它到達您的 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
widget。它可以同時作為 ScrollView
和 iOS TableView
,因為您可以垂直格式佈局 widget。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
如需有關如何在 Flutter 中佈局 widget 的更多詳細文件,請參閱版面配置教學。
手勢偵測與觸控事件處理
#本節討論如何在 Flutter 中偵測手勢和處理不同的事件,以及它們與 UIKit 的比較。
新增點擊監聽器
#在 UIKit 中,您可以將 GestureRecognizer
附加到視圖來處理點擊事件。在 Flutter 中,有兩種新增觸控監聽器的方法:
- 如果 widget 支援事件偵測,請將函式傳遞給它,並在該函式中處理事件。例如,
ElevatedButton
widget 具有onPressed
參數:
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果 widget 不支援事件偵測,請將 widget 包裝在 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
,您可以監聽各種手勢,例如:
點擊(Tapping)
onTapDown
- 可能導致點擊的指標已在特定位置接觸到螢幕。
onTapUp
- 觸發點擊的指標已停止在特定位置接觸螢幕。
onTap
- 已發生點擊。
onTapCancel
- 先前觸發
onTapDown
的指標不會導致點擊。
雙擊(Double tapping)
onDoubleTap
- 使用者在快速連續的時間內在螢幕上的相同位置點擊了兩次。
長按(Long pressing)
onLongPress
- 指標在同一個位置與螢幕保持接觸很長一段時間。
垂直拖曳(Vertical dragging)
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 元件,請宣告一個頂層 widget,MaterialApp
,作為應用程式的進入點。MaterialApp
是一個便利的 widget,它包裝了許多實作 Material Design 的應用程式通常需要的 widget。它在 WidgetsApp
的基礎上,新增了 Material 特定的功能。
但 Flutter 非常彈性且具有表現力,足以實作任何設計語言。在 iOS 上,您可以使用 Cupertino 函式庫 來產生符合人機介面指南的介面。如需這些 widget 的完整集合,請參閱 Cupertino widget 圖庫。
您也可以使用 WidgetsApp
作為您的應用程式 widget,它提供了一些相同的功能,但不如 MaterialApp
豐富。
若要自訂任何子元件的顏色和樣式,請將 ThemeData
物件傳遞給 MaterialApp
widget。例如,在下面的程式碼中,種子中的配色方案設定為深紫色,分隔符號顏色為灰色。
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
物件,您可以在其中自訂許多參數,例如
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
在應用程式中綁定圖片
#雖然 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.jpeg'),
或直接在 Image
widget 中存取
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
如需更多詳細資訊,請參閱在 Flutter 中新增資源和圖片。
表單輸入
#本節討論如何在 Flutter 中使用表單,以及它們與 UIKit 的比較。
取得使用者輸入
#鑑於 Flutter 如何使用具有獨立狀態的不可變 widget,您可能會想知道使用者輸入如何融入其中。在 UIKit 中,當需要提交使用者輸入或對其執行操作時,您通常會查詢 widget 的目前值。這在 Flutter 中是如何運作的?
實際上,表單像 Flutter 中的所有內容一樣,由專用的 widget 處理。如果您有 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 食譜。
文字欄位中的佔位符
#在 Flutter 中,您可以輕鬆地為您的欄位顯示「提示」或佔位符文字,方法是將 InputDecoration
物件新增到 Text
widget 的裝飾建構函式參數
Center(
child: TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
),
)
顯示驗證錯誤
#就像使用「提示」一樣,將 InputDecoration
物件傳遞給 Text
widget 的裝飾建構函式。
但是,您不希望一開始就顯示錯誤。相反,當使用者輸入無效資料時,更新狀態並傳遞新的 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
,讓 Dart 完成繁重的工作,而不會導致 UI 卡住,即可執行網路程式碼
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);
});
}
一旦 await
ed 網路呼叫完成,請透過呼叫 setState()
來更新 UI,這會觸發 widget 子樹的重建並更新資料。
以下範例非同步載入資料,並在 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, dynamic>> data = <Map<String, dynamic>>[];
@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);
});
}
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);
});
}
這通常是您執行網路或資料庫呼叫的方式,這兩者都是 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, dynamic>> data = <Map<String, dynamic>>[];
@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);
});
}
顯示長時間執行的任務進度
#在 UIKit 中,您通常會在背景執行長時間執行的任務時使用 UIProgressView
。
在 Flutter 中,使用 ProgressIndicator
widget。透過控制它何時透過布林旗標呈現來以程式設計方式顯示進度。在您的長時間執行任務開始之前,告知 Flutter 更新其狀態,並在任務結束後隱藏它。
在下面的範例中,build 函式分為三個不同的函式。如果 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, dynamic>> data = <Map<String, dynamic>>[];
@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);
});
}
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(),
);
}
}
除非另有說明,否則本網站上的文件反映 Flutter 的最新穩定版本。頁面最後更新於 2024-09-09。 檢視原始碼 或回報問題。