跳至主要內容
目錄

適用於 Xamarin.Forms 開發人員的 Flutter

目錄

本文件適用於希望將其現有知識應用於使用 Flutter 建置行動應用程式的 Xamarin.Forms 開發人員。如果您了解 Xamarin.Forms 框架的基本概念,則可以使用本文件作為 Flutter 開發的跳板。

您的 Android 和 iOS 知識和技能組在使用 Flutter 建置時非常重要,因為 Flutter 依賴原生作業系統設定,這與您設定原生 Xamarin.Forms 專案的方式類似。Flutter 框架也類似於您建立可在多個平台上使用的單一 UI 的方式。

本文件可以用作食譜,透過跳躍式瀏覽尋找與您的需求最相關的問題。

專案設定

#

應用程式如何啟動?

#

在 Xamarin.Forms 中,對於每個平台,您都會呼叫 LoadApplication 方法,該方法會建立新的應用程式並啟動您的應用程式。

csharp
LoadApplication(new App());

在 Flutter 中,預設的主進入點是 main,您可以在此處載入您的 Flutter 應用程式。

dart
void main() {
  runApp(const MyApp());
}

在 Xamarin.Forms 中,您會在 Application 類別中將 Page 指派給 MainPage 屬性。

csharp
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

dart
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 DesignMaterialApp widget,也可以使用支援 iOS 風格應用程式的 CupertinoApp widget,或者您可以使用較低階的 WidgetsApp,您可以以任何想要的方式進行自訂。

下列程式碼定義首頁,這是一個有狀態 widget。在 Flutter 中,所有 widget 都是不可變的,但支援兩種 widget:有狀態無狀態。無狀態 widget 的範例是標題、圖示或影像。

以下範例使用 MaterialApp,它將其根頁面保留在 home 屬性中。

dart
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 的生命週期內持續存在。

dart
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 效能不佳。

dart
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 中,頁面或元素的對等項目是什麼?

#

ContentPageTabbedPageFlyoutPage 都是您可能會在 Xamarin.Forms 應用程式中使用的頁面類型。這些頁面接著會保留 Element 來顯示各種控制項。在 Xamarin.Forms 中,EntryButtonElement 的範例。

在 Flutter 中,幾乎所有內容都是 widget。在 Flutter 中,稱為 RoutePage 是一個 widget。按鈕、進度列和動畫控制器都是 widget。在建置路由時,您會建立 widget 樹狀結構。

Flutter 包含 Material Components 程式庫。這些 widget 會實作 Material Design 指導方針。Material Design 是一個彈性的設計系統,針對所有平台 (包括 iOS) 進行最佳化

但是 Flutter 夠彈性且能充分表達,可以實作任何設計語言。例如,在 iOS 上,您可以使用 Cupertino widget 來產生看起來像 Apple 的 iOS 設計語言的介面。

如何更新 Widget?

#

在 Xamarin.Forms 中,每個 PageElement 都是一個有狀態的類別,具有屬性和方法。您會更新屬性來更新 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。常見的 StatelessWidgetText widget。如果您查看 Text widget 的實作,您會發現它繼承自 StatelessWidget

dart
const Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如您所見,Text widget 沒有與之關聯的狀態資訊,它會轉譯其建構函式中傳遞的內容,僅此而已。

但是,如果您想要讓「我喜歡 Flutter」動態變更,例如,在按一下 FloatingActionButton 時變更,該怎麼辦?

若要實現此目的,請將 Text widget 包裝在 StatefulWidget 中,並在使用者按一下按鈕時更新它,如下列範例所示

dart
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

dart
@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 之間切換

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: '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 來建立簡單動畫,其中包括 FadeToTranslateTo 等方法。您會在檢視上使用這些方法來執行所需的動畫。

xml
<Image Source="{Binding MyImage}" x:Name="myImage" />

然後在程式碼後置中,或在行為中,這會使影像在 1 秒鐘內淡入。

csharp
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 淡入標誌

dart
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 有兩個類別可以幫助您在畫布上繪圖:CustomPaintCustomPainter,後者會實作您的演算法以繪製到畫布上。

若要了解如何在 Flutter 中實作簽名繪圖器,請參閱 Collin 在 Custom Paint 上的回答。

dart
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

dart
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 小工具一樣。

dart
@override
Widget build(BuildContext context) {
  return const Center(
    child: CustomButton('Hello'),
  );
}
#

如何在頁面之間導覽?

#

在 Xamarin.Forms 中,NavigationPage 類別提供階層式的導覽體驗,讓使用者能夠在頁面之間前後導覽。

Flutter 也有類似的實作,使用 NavigatorRoutesRoute 是應用程式 Page 的抽象概念,而 Navigator 是一個管理路由的 小工具

路由大致對應到一個 Page。導覽器的工作方式與 Xamarin.Forms 的 NavigationPage 類似,它可以根據您是要導覽到視圖還是從視圖返回來 push()pop() 路由。

要在頁面之間導覽,您有幾個選項

  • 指定路由名稱的 Map。(MaterialApp)
  • 直接導覽到路由。(WidgetsApp)

以下範例會建構一個 Map

dart
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 來導覽到該路由。

dart
Navigator.of(context).pushNamed('/b');

Navigator 是一個管理應用程式路由的堆疊。將路由推送到堆疊會移動到該路由。從堆疊中彈出路由會返回到前一個路由。這是透過等待 push() 返回的 Future 來完成的。

async/await 與 .NET 的實作非常相似,在 Async UI 中有更詳細的說明。

例如,要啟動一個讓使用者選擇其位置的 location 路由,您可能會執行以下操作

dart
Object? coordinates = await Navigator.of(context).pushNamed('/location');

然後,在您的「location」路由中,一旦使用者選擇了他們的位置,就使用結果彈出堆疊

dart
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 停止回應來執行網路程式碼

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);
  });
}

一旦等待的網路呼叫完成,請呼叫 setState() 來更新 UI,這會觸發 widget 子樹的重建並更新資料。

以下範例會非同步載入資料並將其顯示在 ListView

dart
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 長時間執行的任務

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);
  });
}

這通常是您執行網路或資料庫呼叫的方式,這兩者都是 I/O 操作。

但是,有時您可能會處理大量資料,並且 UI 會停止回應。在 Flutter 中,請使用 Isolate 來利用多個 CPU 核心來執行長時間執行或計算密集型的任務。

Isolate 是獨立的執行緒,不與主執行記憶體堆積共用任何記憶體。這是 Task.Run() 之間的差異。這表示您無法存取主執行緒中的變數,或透過呼叫 setState() 來更新 UI。

以下範例展示如何在簡單的 isolate 中,將資料共用回主執行緒以更新 UI。

dart
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),或執行計算密集型的數學運算,例如加密或訊號處理。

您可以執行以下的完整範例

dart
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 中的相依性

yaml
dependencies:
  http: ^1.1.0

若要發出網路要求,請呼叫 async 函式 http.get()await

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);
  });
}

如何顯示長時間運作任務的進度?

#

在 Xamarin.Forms 中,您通常會建立載入指示器,可以直接在 XAML 中建立,或透過協力廠商外掛程式 (例如 AcrDialogs) 建立。

在 Flutter 中,請使用 ProgressIndicator 小工具。透過布林旗標控制何時轉譯,以程式設計方式顯示進度。告訴 Flutter 在您的長時間執行任務開始之前更新其狀態,並在其結束後隱藏它。

在以下範例中,建構函式會分成三個不同的函式。如果 showLoadingDialogtrue (當 widgets.length == 0 時),則轉譯 ProgressIndicator。否則,轉譯 ListView,其中包含從網路呼叫傳回的資料。

dart
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.0x2.0x3.0x 或任何其他乘數。Flutter 沒有 dp,但有邏輯像素,它們基本上與裝置獨立像素相同。Flutter 的 devicePixelRatio 表示單一邏輯像素中實體像素的比率。

與 Android 的密度區塊等效的是

Android 密度限定詞Flutter 像素比率
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.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 檔案中宣告這些圖片

yaml
assets:
 - images/my_icon.jpeg

您可以在 Image.asset 小工具中直接存取您的圖片

dart
@override
Widget build(BuildContext context) {
  return Image.asset('images/my_icon.png');
}

或使用 AssetImage

dart
@override
Widget build(BuildContext context) {
  return const Image(
    image: AssetImage('images/my_image.png'),
  );
}

如需更詳細的資訊,請參閱新增資產和圖片

我應該將字串儲存在哪裡?如何處理本地化?

#

與具有 resx 檔案的 .NET 不同,Flutter 目前沒有專用的系統來處理字串。目前,最佳做法是在類別中將您的複製文字宣告為靜態欄位,並從那裡存取它們。例如

dart
class Strings {
  static const String welcomeMessage = 'Welcome To Flutter';
}

您可以這樣存取您的字串

dart
Text(Strings.welcomeMessage);

預設情況下,Flutter 的字串僅支援美國英文。如果您需要新增對其他語言的支援,請加入 flutter_localizations 套件。您可能還需要新增 Dart 的 intl 套件,才能使用 i10n 機制,例如日期/時間格式設定。

yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: any # Use version of intl from flutter_localizations.

若要使用 flutter_localizations 套件,請在應用程式小工具上指定 localizationsDelegatessupportedLocales

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 套件的範例程式碼。

我的專案檔案在哪裡?

#

在 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,其中包含 OnStartOnResumeOnSleep。在 Flutter 中,您可以透過連接到 WidgetsBinding 觀察器並監聽 didChangeAppLifecycleState() 變更事件來監聽類似的生命週期事件。

可觀察的生命週期事件包括

inactive(非活動)
應用程式處於非活動狀態,且未接收使用者輸入。此事件僅限 iOS。
paused(已暫停)
應用程式目前對使用者不可見,且不回應使用者輸入,但正在背景執行。
resumed(已恢復)
應用程式可見並回應使用者輸入。
suspending(暫停中)
應用程式暫時暫停。此事件僅限 Android。

如需這些狀態含義的更多詳細資訊,請參閱 AppLifecycleStatus 文件

版面配置

#

StackLayout 的等效是什麼?

#

在 Xamarin.Forms 中,您可以建立一個 StackLayout,其 Orientation 為水平或垂直。Flutter 有類似的方法,但是您會使用 RowColumn 小工具。

如果您注意到這兩個程式碼範例相同,除了 RowColumn 小工具之外。子項是相同的,此功能可以用於開發豐富的版面配置,這些版面配置可以隨著時間推移與相同的子項一起變更。

dart
@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'),
    ],
  );
}
dart
@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 會提供自動捲動。

dart
@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 小工具來達成此目的。

此範例會建立兩個彼此重疊的圖示。

dart
@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 小工具。您只需將要捲動的內容填入小工具中即可。

dart
@override
Widget build(BuildContext context) {
  return const SingleChildScrollView(
    child: Text('Long Content'),
  );
}

如果您有許多想要包裝在捲軸中的項目,甚至是不同 Widget 類型,您可能想要使用 ListView。這看起來可能有點過頭,但在 Flutter 中,這比 Xamarin.Forms ListView 更優化且更省資源,因為後者是以平台特定的控制項為基礎。

dart
@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 屬性來自動處理橫向轉換。

xml
<activity android:configChanges="orientation|screenSize" />

手勢偵測和觸控事件處理

#

如何在 Flutter 中將 GestureRecognizers 新增至 widget?

#

在 Xamarin.Forms 中,Element 可能包含您可以附加到的點擊事件。許多元素也包含一個與此事件相關聯的 Command。或者,您會使用 TapGestureRecognizer。在 Flutter 中,有兩種非常相似的方法

  1. 如果小工具支援事件偵測,請將函式傳遞給它,並在函式中處理它。例如,ElevatedButton 有一個 onPressed 參數

    dart
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
  2. 如果小工具不支援事件偵測,請將小工具包裝在 GestureDetector 中,並將函式傳遞給 onTap 參數。

    dart
    class 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。您通常會受限於 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizerSwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非您建立自己的手勢辨識器。

在 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 標誌

dart
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 中,後者會使用 DataTemplateSelectorViewCell 傳回的內容來呈現每一列。但是,您通常必須確保開啟 Cell Recycling,否則會遇到記憶體問題和捲動速度緩慢的問題。

由於 Flutter 的不可變小工具模式,您會將小工具清單傳遞到您的 ListView 中,而 Flutter 會負責確保捲動快速且平穩。

dart
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 方法,可找出按下了哪個項目。您可能還使用過許多其他技術,例如檢查 SelectedItemEventToCommand 行為何時變更。

在 Flutter 中,請使用傳入的小工具提供的觸控處理。

dart
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,並將資料從舊清單複製到新清單。雖然這種方法很簡單,但不建議用於大型資料集,如下一個範例所示。

dart
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 相對應,後者會自動為您回收清單元素

dart
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 檔案中參考它,類似於您匯入影像的方式。

yaml
fonts:
  - family: MyCustomFont
    fonts:
      - asset: fonts/MyCustomFont.ttf
      - style: italic

然後將字型指派給您的 Text 小工具

dart
@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 中,擷取資訊是由特殊的小工具處理的,這與您習慣的方式不同。如果您有 TextFieldTextFormField,您可以提供一個 TextEditingController 來擷取使用者輸入

dart
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 屬性。例如

xml
<Entry Placeholder="This is a hint">

在 Flutter 中,您可以透過將 InputDecoration 物件新增至文字小工具的 decoration 建構函式參數,輕鬆地為您的輸入顯示「提示」或預留位置文字。

dart
TextField(
  decoration: InputDecoration(hintText: 'This is a hint'),
),

如何顯示驗證錯誤?

#

在 Xamarin.Forms 中,如果您想要提供驗證錯誤的視覺提示,您需要建立新的屬性和 VisualElement,包圍有驗證錯誤的 Element

在 Flutter 中,你會將一個 InputDecoration 物件傳遞給文字小部件的 decoration 建構函式。

然而,你並不想一開始就顯示錯誤。相反地,當使用者輸入了無效的資料時,更新狀態,並傳遞一個新的 InputDecoration 物件。

dart
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 應用程式仍然會作為一個視圖託管在原生應用程式的 ViewControllerActivity 中,但你無法直接存取此視圖或原生框架。

這並不表示 Flutter 應用程式無法與這些原生 API 或你擁有的任何原生程式碼互動。Flutter 提供了平台通道,用於與託管你的 Flutter 視圖的 ViewControllerActivity 進行通訊和交換資料。平台通道本質上是一個非同步的訊息傳遞機制,它將 Dart 程式碼與託管的 ViewControllerActivity 以及其運行的 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 團隊維護。

你還可以在 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 的色彩配置會設定為深紫色,而文字選取顏色會設定為紅色。

dart
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 外掛程式文件。