適用於 Xamarin.Forms 開發人員的 Flutter
- 專案設定
- 檢視畫面
- 導覽
- 非同步 UI
- 專案結構 & 資源
- 應用程式生命週期
- 版面配置
- 手勢偵測和觸控事件處理
- ListViews 和配接器
- 處理文字
- 表單輸入
- Flutter 外掛程式
- 與硬體、第三方服務和平台互動
- 佈景主題 (樣式)
- 資料庫和本機儲存空間
- 偵錯
- 通知
本文件適用於希望將其現有知識應用於使用 Flutter 建置行動應用程式的 Xamarin.Forms 開發人員。如果您了解 Xamarin.Forms 框架的基本概念,則可以使用本文件作為 Flutter 開發的跳板。
您的 Android 和 iOS 知識和技能組在使用 Flutter 建置時非常重要,因為 Flutter 依賴原生作業系統設定,這與您設定原生 Xamarin.Forms 專案的方式類似。Flutter 框架也類似於您建立可在多個平台上使用的單一 UI 的方式。
本文件可以用作食譜,透過跳躍式瀏覽尋找與您的需求最相關的問題。
專案設定
#應用程式如何啟動?
#在 Xamarin.Forms 中,對於每個平台,您都會呼叫 LoadApplication
方法,該方法會建立新的應用程式並啟動您的應用程式。
LoadApplication(new App());
在 Flutter 中,預設的主進入點是 main
,您可以在此處載入您的 Flutter 應用程式。
void main() {
runApp(const MyApp());
}
在 Xamarin.Forms 中,您會在 Application
類別中將 Page
指派給 MainPage
屬性。
public class App : Application
{
public App()
{
MainPage = new ContentPage
{
Content = new Label
{
Text = "Hello World",
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
}
};
}
}
在 Flutter 中,「一切皆為 widget」,甚至是應用程式本身。以下範例顯示 MyApp
,一個簡單的應用程式 Widget
。
class MyApp extends StatelessWidget {
/// This widget is the root of your application.
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Hello World!',
textDirection: TextDirection.ltr,
),
);
}
}
如何建立頁面?
#Xamarin.Forms 有許多類型的頁面;ContentPage
是最常見的頁面。在 Flutter 中,您會指定一個應用程式 widget,其中包含您的根頁面。您可以使用支援 Material Design 的 MaterialApp
widget,也可以使用支援 iOS 風格應用程式的 CupertinoApp
widget,或者您可以使用較低階的 WidgetsApp
,您可以以任何想要的方式進行自訂。
下列程式碼定義首頁,這是一個有狀態 widget。在 Flutter 中,所有 widget 都是不可變的,但支援兩種 widget:有狀態和無狀態。無狀態 widget 的範例是標題、圖示或影像。
以下範例使用 MaterialApp
,它將其根頁面保留在 home
屬性中。
class MyApp extends StatelessWidget {
/// This widget is the root of your application.
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
從這裡開始,您的實際首頁是另一個 Widget
,您可以在其中建立您的狀態。
有狀態 widget (如下方的 MyHomePage
) 由兩個部分組成。第一部分本身是不可變的,會建立一個 State
物件,其中包含物件的狀態。State
物件會在 widget 的生命週期內持續存在。
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
State
物件會為有狀態 widget 實作 build()
方法。
當 widget 樹狀結構的狀態變更時,請呼叫 setState()
,這會觸發 UI 該部分的建置。請務必僅在必要時呼叫 setState()
,並且僅對 widget 樹狀結構中已變更的部分呼叫,否則可能會導致 UI 效能不佳。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Take the value from the MyHomePage object that was created by
// the App.build method, and use it to set the appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
在 Flutter 中,UI (也稱為 widget 樹狀結構) 是不可變的,這表示您一旦建置完成就無法變更其狀態。您可以在 State
類別中變更欄位,然後呼叫 setState()
以再次重建整個 widget 樹狀結構。
這種產生 UI 的方式與 Xamarin.Forms 不同,但這種方法有很多好處。
檢視畫面
#在 Flutter 中,頁面或元素的對等項目是什麼?
#ContentPage
、TabbedPage
、FlyoutPage
都是您可能會在 Xamarin.Forms 應用程式中使用的頁面類型。這些頁面接著會保留 Element
來顯示各種控制項。在 Xamarin.Forms 中,Entry
或 Button
是 Element
的範例。
在 Flutter 中,幾乎所有內容都是 widget。在 Flutter 中,稱為 Route
的 Page
是一個 widget。按鈕、進度列和動畫控制器都是 widget。在建置路由時,您會建立 widget 樹狀結構。
Flutter 包含 Material Components 程式庫。這些 widget 會實作 Material Design 指導方針。Material Design 是一個彈性的設計系統,針對所有平台 (包括 iOS) 進行最佳化。
但是 Flutter 夠彈性且能充分表達,可以實作任何設計語言。例如,在 iOS 上,您可以使用 Cupertino widget 來產生看起來像 Apple 的 iOS 設計語言的介面。
如何更新 Widget?
#在 Xamarin.Forms 中,每個 Page
或 Element
都是一個有狀態的類別,具有屬性和方法。您會更新屬性來更新 Element
,並將其傳播到原生控制項。
在 Flutter 中,Widget
是不可變的,您無法直接透過變更屬性來更新它們,而是必須使用 widget 的狀態。
這就是有狀態與無狀態 widget 的概念的由來。StatelessWidget
名符其實 — 一個沒有狀態資訊的 widget。
當您所描述的使用者介面部分不依賴於物件中的組態資訊以外的任何內容時,StatelessWidgets
會很有用。
例如,在 Xamarin.Forms 中,這類似於放置具有您的標誌的 Image
。標誌在執行階段不會變更,因此請在 Flutter 中使用 StatelessWidget
。
如果您想要根據發出 HTTP 呼叫或使用者互動後收到的資料來動態變更 UI,則必須使用 StatefulWidget
並告知 Flutter 框架 widget 的 State
已更新,因此它可以更新該 widget。
這裡要注意的重點是,在核心中,無狀態和有狀態 widget 的行為方式相同。它們會在每個影格重建,不同之處在於 StatefulWidget
具有一個 State
物件,該物件會在多個影格之間儲存狀態資料並還原它。
如果您有疑問,請始終記住這條規則:如果 widget 變更 (例如,由於使用者互動),則它是「有狀態的」。但是,如果 widget 對變更做出反應,則如果它本身不對變更做出反應,則包含的父 widget 仍然可以是「無狀態的」。
以下範例顯示如何使用 StatelessWidget
。常見的 StatelessWidget
是 Text
widget。如果您查看 Text
widget 的實作,您會發現它繼承自 StatelessWidget
。
const Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如您所見,Text
widget 沒有與之關聯的狀態資訊,它會轉譯其建構函式中傳遞的內容,僅此而已。
但是,如果您想要讓「我喜歡 Flutter」動態變更,例如,在按一下 FloatingActionButton
時變更,該怎麼辦?
若要實現此目的,請將 Text
widget 包裝在 StatefulWidget
中,並在使用者按一下按鈕時更新它,如下列範例所示
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
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),
),
);
}
}
如何配置 Widget?XAML 檔案的對等項目是什麼?
#在 Xamarin.Forms 中,大多數開發人員會在 XAML 中編寫版面配置,雖然有時會在 C# 中編寫版面配置。在 Flutter 中,您會在程式碼中使用 widget 樹狀結構來編寫版面配置。
以下範例顯示如何顯示具有填補的簡單 widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.only(left: 20, right: 30),
),
onPressed: () {},
child: const Text('Hello'),
),
),
);
}
您可以在widget 目錄中檢視 Flutter 提供的版面配置。
如何從配置中新增或移除元素?
#在 Xamarin.Forms 中,您必須在程式碼中移除或新增 Element
。這需要設定 Content
屬性,或在它是清單時呼叫 Add()
或 Remove()
。
在 Flutter 中,因為 widget 是不可變的,所以沒有直接等效項。相反地,您可以將函數傳遞給父系,該函數會傳回一個 widget,並使用布林旗標控制該子系的建立。
以下範例顯示當使用者按一下 FloatingActionButton
時如何在兩個 widget 之間切換
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),
),
);
}
}
如何為 Widget 製作動畫?
#在 Xamarin.Forms 中,您可以使用 ViewExtensions 來建立簡單動畫,其中包括 FadeTo
和 TranslateTo
等方法。您會在檢視上使用這些方法來執行所需的動畫。
<Image Source="{Binding MyImage}" x:Name="myImage" />
然後在程式碼後置中,或在行為中,這會使影像在 1 秒鐘內淡入。
myImage.FadeTo(0, 1000);
在 Flutter 中,您可以使用動畫程式庫來產生 widget 的動畫效果,方法是將 widget 包裝在動畫 widget 內。使用 AnimationController
,這是一個可以暫停、搜尋、停止和反轉動畫的 Animation<double>
。它需要一個 Ticker
,它會發出訊號告知何時發生 vsync,並在執行時在每個影格上產生 0 和 1 之間的線性內插。然後,您會建立一個或多個 Animation
,並將它們附加到控制器。
例如,您可以使用 CurvedAnimation
來沿著內插曲線實作動畫。從這個意義上來說,控制器是動畫進度的「主要」來源,而 CurvedAnimation
會計算取代控制器預設線性動作的曲線。與 widget 一樣,Flutter 中的動畫也使用組合。
在建置 widget 樹狀結構時,您會將 Animation
指派給 widget 的動畫屬性 (例如 FadeTransition
的不透明度),並告知控制器開始動畫。
以下範例顯示如何編寫 FadeTransition
,當您按下 FloatingActionButton
時,該轉換會將 widget 淡入標誌
import 'package:flutter/material.dart';
void main() {
runApp(const FadeAppTest());
}
class FadeAppTest extends StatelessWidget {
/// This widget is the root of your application.
const FadeAppTest({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 TickerProviderStateMixin {
late final AnimationController controller;
late final CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
}
@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),
),
);
}
}
如需更多資訊,請參閱 動畫 & 動態 widget、動畫教學課程和 動畫概觀。
如何繪製/繪圖在畫面上?
#Xamarin.Forms 從來沒有內建直接在螢幕上繪圖的方式。許多人如果需要繪製自訂圖片,會使用 SkiaSharp。在 Flutter 中,您可以直接存取 Skia Canvas,並輕鬆在螢幕上繪圖。
Flutter 有兩個類別可以幫助您在畫布上繪圖:CustomPaint
和 CustomPainter
,後者會實作您的演算法以繪製到畫布上。
若要了解如何在 Flutter 中實作簽名繪圖器,請參閱 Collin 在 Custom Paint 上的回答。
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
SignatureState createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset?>[];
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
final RenderBox referenceBox = context.findRenderObject() as RenderBox;
final Offset localPosition = referenceBox.globalToLocal(
details.globalPosition,
);
_points = List.from(_points)..add(localPosition);
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: _onPanUpdate,
onPanEnd: (details) => _points.add(null),
child: CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
);
}
}
class SignaturePainter extends CustomPainter {
const 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;
}
Widget 的不透明度在哪裡?
#在 Xamarin.Forms 中,所有 VisualElement
都有 Opacity 屬性。在 Flutter 中,您需要將小工具包裝在 Opacity
小工具中才能達成此目的。
如何建立自訂 Widget?
#在 Xamarin.Forms 中,您通常會繼承 VisualElement
,或使用現有的 VisualElement
,來覆寫並實作方法以達成所需的行為。
在 Flutter 中,透過組合較小的 widget (而不是繼承它們) 來建構自訂 widget。這有點類似於實作一個基於 Grid
的自訂控制項,其中加入許多 VisualElement
,同時擴展自訂邏輯。
例如,如何建構一個在建構函式中接收標籤的 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'),
);
}
導覽
#如何在頁面之間導覽?
#在 Xamarin.Forms 中,NavigationPage
類別提供階層式的導覽體驗,讓使用者能夠在頁面之間前後導覽。
Flutter 也有類似的實作,使用 Navigator
和 Routes
。Route
是應用程式 Page
的抽象概念,而 Navigator
是一個管理路由的 小工具。
路由大致對應到一個 Page
。導覽器的工作方式與 Xamarin.Forms 的 NavigationPage
類似,它可以根據您是要導覽到視圖還是從視圖返回來 push()
和 pop()
路由。
要在頁面之間導覽,您有幾個選項
- 指定路由名稱的
Map
。(MaterialApp
) - 直接導覽到路由。(
WidgetsApp
)
以下範例會建構一個 Map
。
void main() {
runApp(
MaterialApp(
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'),
},
),
);
}
將路由名稱推送到 Navigator
來導覽到該路由。
Navigator.of(context).pushNamed('/b');
Navigator
是一個管理應用程式路由的堆疊。將路由推送到堆疊會移動到該路由。從堆疊中彈出路由會返回到前一個路由。這是透過等待 push()
返回的 Future
來完成的。
async
/await
與 .NET 的實作非常相似,在 Async UI 中有更詳細的說明。
例如,要啟動一個讓使用者選擇其位置的 location
路由,您可能會執行以下操作
Object? coordinates = await Navigator.of(context).pushNamed('/location');
然後,在您的「location」路由中,一旦使用者選擇了他們的位置,就使用結果彈出堆疊
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
如何導覽至另一個應用程式?
#在 Xamarin.Forms 中,要將使用者傳送到另一個應用程式,您可以使用特定的 URI 方案,使用 Device.OpenUrl("mailto://")
。
要在 Flutter 中實作此功能,請建立原生平台整合,或使用現有的外掛程式,例如url_launcher
,在 pub.dev 上有許多其他套件可用。
非同步 UI
#在 Flutter 中,Device.BeginOnMainThread()
的等效是什麼?
#Dart 有單執行緒的執行模型,支援 Isolate
(一種在另一個執行緒上執行 Dart 程式碼的方式)、事件迴圈和非同步程式設計。除非您產生一個 Isolate
,否則您的 Dart 程式碼會在主 UI 執行緒中執行,並由事件迴圈驅動。
Dart 的單執行緒模型並不表示您需要將所有內容都作為會導致 UI 凍結的封鎖操作來執行。與 Xamarin.Forms 非常相似,您需要保持 UI 執行緒空閒。您將使用 async
/await
來執行必須等待回應的任務。
在 Flutter 中,使用 Dart 語言提供的非同步工具 (也稱為 async
/await
) 來執行非同步工作。這與 C# 非常相似,對於任何 Xamarin.Forms 開發人員來說都應該非常容易使用。
例如,您可以使用 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);
});
}
一旦等待的網路呼叫完成,請呼叫 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 與 Android 的差異的資訊。
如何將工作移至背景執行緒?
#由於 Flutter 是單執行緒並執行事件迴圈,因此您不必擔心執行緒管理或產生背景執行緒。這與 Xamarin.Forms 非常相似。如果您正在執行 I/O 繫結的工作 (例如磁碟存取或網路呼叫),則可以安全地使用 async
/await
,這樣就完成了。
另一方面,如果您需要執行計算密集型的工作 (讓 CPU 忙碌),則需要將其移至 Isolate
以避免封鎖事件迴圈,就像您會將任何類型的工作移出主執行緒一樣。這與您在 Xamarin.Forms 中透過 Task.Run()
將內容移至不同的執行緒時類似。
對於 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 是獨立的執行緒,不與主執行記憶體堆積共用任何記憶體。這是 Task.Run()
之間的差異。這表示您無法存取主執行緒中的變數,或透過呼叫 setState()
來更新 UI。
以下範例展示如何在簡單的 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() {
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 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: getBody(),
);
}
}
如何發送網路請求?
#在 Xamarin.Forms 中,您會使用 HttpClient
。當您使用熱門的 http
套件時,在 Flutter 中進行網路呼叫很容易。這會抽象化許多您通常會自己實作的網路,使網路呼叫變得簡單。
若要使用 http
套件,請將其新增至 pubspec.yaml
中的相依性
dependencies:
http: ^1.1.0
若要發出網路要求,請呼叫 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);
});
}
如何顯示長時間運作任務的進度?
#在 Xamarin.Forms 中,您通常會建立載入指示器,可以直接在 XAML 中建立,或透過協力廠商外掛程式 (例如 AcrDialogs) 建立。
在 Flutter 中,請使用 ProgressIndicator
小工具。透過布林旗標控制何時轉譯,以程式設計方式顯示進度。告訴 Flutter 在您的長時間執行任務開始之前更新其狀態,並在其結束後隱藏它。
在以下範例中,建構函式會分成三個不同的函式。如果 showLoadingDialog
為 true
(當 widgets.length == 0
時),則轉譯 ProgressIndicator
。否則,轉譯 ListView
,其中包含從網路呼叫傳回的資料。
import 'dart:async';
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 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: getBody(),
);
}
}
專案結構 & 資源
#我應該將圖片檔案儲存在哪裡?
#Xamarin.Forms 沒有平台獨立的方式來儲存圖片,您必須將圖片放置在 iOS 上的 xcasset
資料夾中,或放置在 Android 上的各種 drawable
資料夾中。
雖然 Android 和 iOS 將資源和資產視為不同的項目,但 Flutter 應用程式只有資產。所有原本會在 Android 上 Resources/drawable-*
資料夾中的資源,都會放置在 Flutter 的資產資料夾中。
Flutter 遵循與 iOS 類似的簡單密度格式。資產可能是 1.0x
、2.0x
、3.0x
或任何其他乘數。Flutter 沒有 dp
,但有邏輯像素,它們基本上與裝置獨立像素相同。Flutter 的 devicePixelRatio
表示單一邏輯像素中實體像素的比率。
與 Android 的密度區塊等效的是
Android 密度限定詞 | Flutter 像素比率 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
資產位於任何任意資料夾中 — 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.jpeg
您可以在 Image.asset
小工具中直接存取您的圖片
@override
Widget build(BuildContext context) {
return Image.asset('images/my_icon.png');
}
或使用 AssetImage
@override
Widget build(BuildContext context) {
return const Image(
image: AssetImage('images/my_image.png'),
);
}
如需更詳細的資訊,請參閱新增資產和圖片。
我應該將字串儲存在哪裡?如何處理本地化?
#與具有 resx
檔案的 .NET 不同,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_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
套件的範例程式碼。
我的專案檔案在哪裡?
#在 Xamarin.Forms 中,您會有一個 csproj
檔案。Flutter 中最接近的對應項目是 pubspec.yaml,其中包含套件相依性和各種專案詳細資料。與 .NET Standard 類似,同一個目錄中的檔案會被視為專案的一部分。
NuGet 的等效是什麼?如何新增相依性?
#在 .NET 生態系統中,原生 Xamarin 專案和 Xamarin.Forms 專案可以存取 Nuget 和內建的套件管理系統。Flutter 應用程式包含原生 Android 應用程式、原生 iOS 應用程式和 Flutter 應用程式。
在 Android 中,您透過新增至 Gradle 建置腳本來新增相依性。在 iOS 中,您透過新增至 Podfile
來新增相依性。
Flutter 使用 Dart 自己的建置系統和 Pub 套件管理器。這些工具會將原生 Android 和 iOS 包裝應用程式的建置委派給各自的建置系統。
一般來說,使用 pubspec.yaml
來宣告要在 Flutter 中使用的外部相依性。在 pub.dev 上可以找到許多 Flutter 套件。
應用程式生命週期
#如何監聽應用程式生命週期事件?
#在 Xamarin.Forms 中,您有一個 Application
,其中包含 OnStart
、OnResume
和 OnSleep
。在 Flutter 中,您可以透過連接到 WidgetsBinding
觀察器並監聽 didChangeAppLifecycleState()
變更事件來監聽類似的生命週期事件。
可觀察的生命週期事件包括
inactive(非活動)
- 應用程式處於非活動狀態,且未接收使用者輸入。此事件僅限 iOS。
paused(已暫停)
- 應用程式目前對使用者不可見,且不回應使用者輸入,但正在背景執行。
resumed(已恢復)
- 應用程式可見並回應使用者輸入。
suspending(暫停中)
- 應用程式暫時暫停。此事件僅限 Android。
如需這些狀態含義的更多詳細資訊,請參閱 AppLifecycleStatus
文件。
版面配置
#StackLayout
的等效是什麼?
#在 Xamarin.Forms 中,您可以建立一個 StackLayout
,其 Orientation
為水平或垂直。Flutter 有類似的方法,但是您會使用 Row
或 Column
小工具。
如果您注意到這兩個程式碼範例相同,除了 Row
和 Column
小工具之外。子項是相同的,此功能可以用於開發豐富的版面配置,這些版面配置可以隨著時間推移與相同的子項一起變更。
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
@override
Widget build(BuildContext context) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
Grid
的等效是什麼?
#最接近 Grid
的對等物是 GridView
。這比您在 Xamarin.Forms 中習慣的要強大得多。當內容超出其可檢視空間時,GridView
會提供自動捲動。
@override
Widget build(BuildContext context) {
return GridView.count(
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this would produce 2 rows.
crossAxisCount: 2,
// Generate 100 widgets that display their index in the list.
children: List<Widget>.generate(
100,
(index) {
return Center(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headlineMedium,
),
);
},
),
);
}
您可能在 Xamarin.Forms 中使用 Grid
來實作覆蓋其他小工具的小工具。在 Flutter 中,您可以使用 Stack
小工具來達成此目的。
此範例會建立兩個彼此重疊的圖示。
@override
Widget build(BuildContext context) {
return const Stack(
children: <Widget>[
Icon(
Icons.add_box,
size: 24,
color: Colors.black,
),
Positioned(
left: 10,
child: Icon(
Icons.add_circle,
size: 24,
color: Colors.black,
),
),
],
);
}
ScrollView
的等效是什麼?
#在 Xamarin.Forms 中,ScrollView
會包裝在 VisualElement
周圍,如果內容大於裝置螢幕,則會捲動。
在 Flutter 中,最接近的對應項目是 SingleChildScrollView
小工具。您只需將要捲動的內容填入小工具中即可。
@override
Widget build(BuildContext context) {
return const SingleChildScrollView(
child: Text('Long Content'),
);
}
如果您有許多想要包裝在捲軸中的項目,甚至是不同 Widget
類型,您可能想要使用 ListView
。這看起來可能有點過頭,但在 Flutter 中,這比 Xamarin.Forms ListView
更優化且更省資源,因為後者是以平台特定的控制項為基礎。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
如何在 Flutter 中處理橫向轉變?
#您可以透過在 AndroidManifest.xml 中設定 configChanges
屬性來自動處理橫向轉換。
<activity android:configChanges="orientation|screenSize" />
手勢偵測和觸控事件處理
#如何在 Flutter 中將 GestureRecognizers
新增至 widget?
#在 Xamarin.Forms 中,Element
可能包含您可以附加到的點擊事件。許多元素也包含一個與此事件相關聯的 Command
。或者,您會使用 TapGestureRecognizer
。在 Flutter 中,有兩種非常相似的方法
如果小工具支援事件偵測,請將函式傳遞給它,並在函式中處理它。例如,ElevatedButton 有一個
onPressed
參數dart@override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { developer.log('click'); }, child: const Text('Button'), ); }
如果小工具不支援事件偵測,請將小工具包裝在
GestureDetector
中,並將函式傳遞給onTap
參數。dartclass SampleApp extends StatelessWidget { const SampleApp({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: GestureDetector( onTap: () { developer.log('tap'); }, child: const FlutterLogo(size: 200), ), ), ); } }
如何處理 widget 上的其他手勢?
#在 Xamarin.Forms 中,您會將 GestureRecognizer
新增至 View
。您通常會受限於 TapGestureRecognizer
、PinchGestureRecognizer
、PanGestureRecognizer
、SwipeGestureRecognizer
、DragGestureRecognizer
和 DropGestureRecognizer
,除非您建立自己的手勢辨識器。
在 Flutter 中,使用 GestureDetector,您可以監聽各種手勢,例如
- Tap(點擊)
onTapDown
- 可能會導致點擊的指標已在特定位置接觸螢幕。
onTapUp
- 觸發點擊的指標已停止在特定位置接觸螢幕。
onTap
- 發生點擊。
onTapCancel
- 先前觸發
onTapDown
的指標不會導致點擊。
- Double tap(雙擊)
onDoubleTap
- 使用者在相同位置快速連續點擊螢幕兩次。
- Long press(長按)
onLongPress
- 指標已在相同位置接觸螢幕很長一段時間。
- Vertical drag(垂直拖曳)
onVerticalDragStart
- 指標已接觸螢幕,且可能開始垂直移動。
onVerticalDragUpdate
- 與螢幕接觸的指標已在垂直方向上進一步移動。
onVerticalDragEnd
- 先前與螢幕接觸並垂直移動的指標已不再與螢幕接觸,且在停止接觸螢幕時以特定速度移動。
- Horizontal drag(水平拖曳)
onHorizontalDragStart
- 指標已接觸螢幕,且可能開始水平移動。
onHorizontalDragUpdate
- 與螢幕接觸的指標已在水平方向上進一步移動。
onHorizontalDragEnd
- 先前與螢幕接觸並水平移動的指標已不再與螢幕接觸,且在停止接觸螢幕時以特定速度移動。
以下範例顯示一個 GestureDetector
,它會在雙擊時旋轉 Flutter 標誌
class RotatingFlutterDetector extends StatefulWidget {
const RotatingFlutterDetector({super.key});
@override
State<RotatingFlutterDetector> createState() =>
_RotatingFlutterDetectorState();
}
class _RotatingFlutterDetectorState extends State<RotatingFlutterDetector>
with SingleTickerProviderStateMixin {
late final AnimationController controller;
late final CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
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),
),
),
),
);
}
}
ListViews 和配接器
#Flutter 中 ListView
的等效是什麼?
#Flutter 中與 ListView
對應的項目是 … ListView
!
在 Xamarin.Forms ListView
中,您會建立一個 ViewCell
,並可能建立一個 DataTemplateSelector
,並將其傳遞到 ListView
中,後者會使用 DataTemplateSelector
或 ViewCell
傳回的內容來呈現每一列。但是,您通常必須確保開啟 Cell Recycling,否則會遇到記憶體問題和捲動速度緩慢的問題。
由於 Flutter 的不可變小工具模式,您會將小工具清單傳遞到您的 ListView
中,而 Flutter 會負責確保捲動快速且平穩。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
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 StatelessWidget {
const SampleAppPage({super.key});
List<Widget> _getListData() {
return List<Widget>.generate(
100,
(index) => Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $index'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}
如何知道哪個清單項目被點擊?
#在 Xamarin.Forms 中,ListView 有一個 ItemTapped
方法,可找出按下了哪個項目。您可能還使用過許多其他技術,例如檢查 SelectedItem
或 EventToCommand
行為何時變更。
在 Flutter 中,請使用傳入的小工具提供的觸控處理。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
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> {
List<Widget> _getListData() {
return List<Widget>.generate(
100,
(index) => GestureDetector(
onTap: () {
developer.log('Row $index tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $index'),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
}
如何動態更新 ListView?
#在 Xamarin.Forms 中,如果您將 ItemsSource
屬性繫結到 ObservableCollection
,您只需在 ViewModel 中更新清單即可。或者,您可以將新的 List
指派給 ItemSource
屬性。
在 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 {
/// 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> {
List<Widget> widgets = <Widget>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int index) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List<Widget>.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('Row $index');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $index'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: widgets),
);
}
}
建議、有效率且有效的方法是使用 ListView.Builder
來建置清單。當您有動態清單或具有大量資料的清單時,此方法非常有用。這基本上與 Android 上的 RecyclerView 相對應,後者會自動為您回收清單元素
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
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> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int index) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('Row $index');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $index'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, index) {
return getRow(index);
},
),
);
}
}
不要建立 ListView
,而是建立一個 ListView.builder
,它採用兩個關鍵參數:清單的初始長度和項目建置器函式。
項目建置器函式與 Android 配接器中的 getView
函式類似;它會採用位置,並傳回您想要在該位置呈現的列。
最後,但最重要的是,請注意 onTap()
函式不再重新建立清單,而是改為將其新增至清單。
如需更多資訊,請參閱 您的第一個 Flutter 應用程式程式碼實驗室。
處理文字
#如何在我文字 widget 上設定自訂字型?
#在 Xamarin.Forms 中,您必須在每個原生專案中新增自訂字型。然後,在您的 Element
中,您會使用 filename#fontname
將此字型名稱指派給 FontFamily
屬性,並且針對 iOS 只使用 fontname
。
在 Flutter 中,將字型檔案放在資料夾中,並在 pubspec.yaml
檔案中參考它,類似於您匯入影像的方式。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然後將字型指派給您的 Text
小工具
@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'),
),
),
);
}
如何設定我的文字 widget 的樣式?
#除了字型之外,您還可以在 Text
小工具上自訂其他樣式元素。Text
小工具的樣式參數會採用 TextStyle
物件,您可以在其中自訂許多參數,例如
color(顏色)
decoration(裝飾)
decorationColor(裝飾顏色)
decorationStyle(裝飾樣式)
fontFamily(字型系列)
fontSize(字型大小)
fontStyle(字型樣式)
fontWeight(字型粗細)
hashCode
height(高度)
inherit(繼承)
letterSpacing(字母間距)
textBaseline(文字基準線)
wordSpacing(字詞間距)
表單輸入
#如何擷取使用者輸入?
#Xamarin.Forms element
允許您直接查詢 element
,以判斷其屬性的狀態,或它是否繫結到 ViewModel
中的屬性。
在 Flutter 中,擷取資訊是由特殊的小工具處理的,這與您習慣的方式不同。如果您有 TextField
或 TextFormField
,您可以提供一個 TextEditingController
來擷取使用者輸入
import 'package:flutter/material.dart';
class MyForm extends StatefulWidget {
const MyForm({super.key});
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
/// Create a text controller and use it to retrieve the current value
/// of the TextField.
final TextEditingController 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 that the user has typed into our text field.
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text that the user has entered using the
// TextEditingController.
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: const Icon(Icons.text_fields),
),
);
}
}
您可以在 擷取文字欄位的值 中找到更多資訊和完整的程式碼清單,取自 Flutter 食譜。
在 Entry
上,Placeholder 的等效是什麼?
#在 Xamarin.Forms 中,某些 Elements
支援您可以指派值的 Placeholder
屬性。例如
<Entry Placeholder="This is a hint">
在 Flutter 中,您可以透過將 InputDecoration
物件新增至文字小工具的 decoration
建構函式參數,輕鬆地為您的輸入顯示「提示」或預留位置文字。
TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
),
如何顯示驗證錯誤?
#在 Xamarin.Forms 中,如果您想要提供驗證錯誤的視覺提示,您需要建立新的屬性和 VisualElement
,包圍有驗證錯誤的 Element
。
在 Flutter 中,你會將一個 InputDecoration
物件傳遞給文字小部件的 decoration
建構函式。
然而,你並不想一開始就顯示錯誤。相反地,當使用者輸入了無效的資料時,更新狀態,並傳遞一個新的 InputDecoration
物件。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
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> {
String? _errorText;
String? _getErrorText() {
return _errorText;
}
bool isEmail(String em) {
const 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,}))$';
final 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: _getErrorText(),
),
),
),
);
}
}
Flutter 外掛程式
#與硬體、第三方服務和平台互動
#如何與平台以及平台的原生程式碼互動?
#Flutter 並不會直接在底層平台執行程式碼;而是組成 Flutter 應用程式的 Dart 程式碼會在裝置上原生執行,從而「繞過」平台提供的 SDK。這表示,例如,當你在 Dart 中執行網路請求時,它會直接在 Dart 環境中執行。你不會使用在編寫原生應用程式時通常會利用的 Android 或 iOS API。你的 Flutter 應用程式仍然會作為一個視圖託管在原生應用程式的 ViewController
或 Activity
中,但你無法直接存取此視圖或原生框架。
這並不表示 Flutter 應用程式無法與這些原生 API 或你擁有的任何原生程式碼互動。Flutter 提供了平台通道,用於與託管你的 Flutter 視圖的 ViewController
或 Activity
進行通訊和交換資料。平台通道本質上是一個非同步的訊息傳遞機制,它將 Dart 程式碼與託管的 ViewController
或 Activity
以及其運行的 iOS 或 Android 框架連接起來。你可以使用平台通道在原生端執行方法,或從裝置的感應器擷取一些資料,例如。
除了直接使用平台通道外,你還可以使用各種預先製作的外掛程式,這些外掛程式封裝了特定目標的原生和 Dart 程式碼。例如,你可以使用外掛程式直接從 Flutter 存取相機膠卷和裝置相機,而無需編寫自己的整合。外掛程式可以在 pub.dev(Dart 和 Flutter 的開源套件儲存庫)上找到。有些套件可能支援 iOS 或 Android,或兩者的原生整合。
如果你在 pub.dev 上找不到符合你需求的外掛程式,你可以編寫自己的外掛程式,並將其發布在 pub.dev 上。
如何存取 GPS 感應器?
#使用 geolocator
社群外掛程式。
如何存取相機?
#camera
外掛程式在存取相機方面非常受歡迎。
如何使用 Facebook 登入?
#若要使用 Facebook 登入,請使用 flutter_facebook_login
社群外掛程式。
如何使用 Firebase 功能?
#大多數 Firebase 功能都由第一方外掛程式涵蓋。這些外掛程式是第一方整合,由 Flutter 團隊維護。
google_mobile_ads
用於 Flutter 的 Google Mobile Ads。firebase_analytics
用於 Firebase Analytics。firebase_auth
用於 Firebase Auth。firebase_database
用於 Firebase RTDB。firebase_storage
用於 Firebase Cloud Storage。firebase_messaging
用於 Firebase Messaging (FCM)。flutter_firebase_ui
用於快速 Firebase Auth 整合(Facebook、Google、Twitter 和電子郵件)。cloud_firestore
用於 Firebase Cloud Firestore。
你還可以在 pub.dev 上找到一些第三方 Firebase 外掛程式,這些外掛程式涵蓋了第一方外掛程式未直接涵蓋的領域。
如何建立我自己的自訂原生整合?
#如果 Flutter 或其社群外掛程式缺少特定於平台的功能,你可以按照開發套件和外掛程式頁面中的指示來構建自己的外掛程式。
簡而言之,Flutter 的外掛程式架構很像在 Android 中使用事件總線:你發送一則訊息,並讓接收者處理並將結果發回給你。在這種情況下,接收者是在 Android 或 iOS 的原生端上運行的程式碼。
佈景主題 (樣式)
#如何為我的應用程式設定佈景主題?
#Flutter 具有美觀的內建 Material Design 實作,可處理你通常會做的許多樣式和主題需求。
Xamarin.Forms 確實有一個全域的 ResourceDictionary
,你可以在整個應用程式中共享樣式。或者,目前有主題支援處於預覽狀態。
在 Flutter 中,你在頂層小部件中宣告主題。
若要在你的應用程式中充分利用 Material Components,你可以宣告頂層小部件 MaterialApp
作為應用程式的進入點。MaterialApp
是一個方便的小部件,它封裝了許多實作 Material Design 的應用程式通常需要的小部件。它在 WidgetsApp
的基礎上添加了特定於 Material 的功能。
你也可以使用 WidgetsApp
作為你的應用程式小部件,它提供了一些相同的功能,但不如 MaterialApp
豐富。
若要自訂任何子元件的顏色和樣式,請將 ThemeData
物件傳遞給 MaterialApp
小部件。例如,在以下程式碼中,seed 的色彩配置會設定為深紫色,而文字選取顏色會設定為紅色。
class SampleApp extends StatelessWidget {
/// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
textSelectionTheme:
const TextSelectionThemeData(selectionColor: Colors.red),
),
home: const SampleAppPage(),
);
}
}
資料庫和本機儲存空間
#如何存取共享偏好設定或 UserDefaults
?
#Xamarin.Forms 開發人員可能會熟悉 Xam.Plugins.Settings
外掛程式。
在 Flutter 中,使用 shared_preferences
外掛程式存取等效的功能。此外掛程式封裝了 UserDefaults
和 Android 等效元件 SharedPreferences
的功能。
如何在 Flutter 中存取 SQLite?
#在 Xamarin.Forms 中,大多數應用程式會使用 sqlite-net-pcl
外掛程式來存取 SQLite 資料庫。
在 Flutter 中,在 macOS、Android 和 iOS 上,使用 sqflite
外掛程式存取此功能。
偵錯
#我可以使用哪些工具在 Flutter 中除錯我的應用程式?
#使用 DevTools 套件來偵錯 Flutter 或 Dart 應用程式。
DevTools 包含對效能分析、檢查堆積、檢查小部件樹、記錄診斷、偵錯、觀察已執行程式碼行、偵錯記憶體洩漏和記憶體片段化的支援。如需更多資訊,請查看DevTools 文件。
通知
#如何設定推播通知?
#在 Android 中,你會使用 Firebase Cloud Messaging 為你的應用程式設定推送通知。
在 Flutter 中,使用 firebase_messaging
外掛程式存取此功能。如需更多有關使用 Firebase Cloud Messaging API 的資訊,請參閱firebase_messaging
外掛程式文件。
除非另有說明,否則本網站上的文件反映了最新穩定版的 Flutter。頁面上次更新時間為 2024-06-24。 檢視原始碼 或 回報問題。