跳到主內容

面向 Xamarin.Forms 開發者的 Flutter 指南

學習如何將 Xamarin.Forms 開發經驗應用於構建 Flutter 應用。

本文件旨在幫助 Xamarin.Forms 開發者利用現有知識來構建 Flutter 移動應用。如果你瞭解 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 中,你需要將 Page 賦值給 Application 類中的 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,亦或使用可以完全自定義的底層 WidgetsApp

以下程式碼定義了主頁,這是一個有狀態的(Stateful)Widget。在 Flutter 中,所有 Widget 都是不可變的,但支援兩種 Widget 型別:有狀態(Stateful)無狀態(Stateless)。無狀態 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 中對應的 Page 或 Element 是什麼?

#

ContentPageTabbedPageFlyoutPage 都是你在 Xamarin.Forms 應用中可能會用到的頁面型別。這些頁面會包含用於顯示各種控制元件的 Element。在 Xamarin.Forms 中,EntryButton 都是 Element 的示例。

在 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 與無狀態 Widget 的區別所在。StatelessWidget 顧名思義,就是一個沒有狀態資訊的 Widget。

當 UI 的某一部分描述僅依賴於物件本身的配置資訊時,StatelessWidget 非常有用。

例如,在 Xamarin.Forms 中,這類似於放置一個帶有 logo 的 Image。由於 logo 在執行時不會改變,因此在 Flutter 中應該使用 StatelessWidget

如果你需要根據 HTTP 呼叫獲取的資料或使用者互動動態更改 UI,則必須使用 StatefulWidget,並告知 Flutter 框架該 Widget 的 State 已更新,以便它可以重新渲染該 Widget。

這裡需要注意的重要一點是,無狀態和有狀態 Widget 的核心行為是一樣的。它們在每一幀都會重建,不同之處在於 StatefulWidget 擁有一個 State 物件,該物件可以跨幀儲存和恢復狀態資料。

如有疑問,請記住這條規則:如果 Widget 發生變化(例如由於使用者互動),它就是有狀態的。但是,如果 Widget 只是響應變化,那麼只要包含它的父 Widget 本身不響應變化,父 Widget 仍然可以是無狀態的。

以下示例展示瞭如何使用 StatelessWidget。常用的 Text 元件就是 StatelessWidget。如果你檢視 Text Widget 的實現,會發現它是 StatelessWidget 的子類。

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

正如你所見,Text Widget 沒有關聯任何狀態資訊,它只會渲染建構函式中傳入的內容。

但是,如果你想讓“I Like 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 提供的佈局。

如何從佈局中新增或刪除 Element?

#

在 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 中,你可以使用包含 FadeToTranslateTo 等方法的 ViewExtensions 來建立簡單的動畫。你可以在檢視上呼叫這些方法來執行所需的動畫。

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

在後臺程式碼(code behind)或行為中,這將使影像在 1 秒內漸顯。

csharp
myImage.FadeTo(0, 1000);

在 Flutter 中,你可以透過將 Widget 包裝在動畫 Widget 中來使用動畫庫。使用 AnimationController,它是一個 Animation<double>,可以暫停、查詢、停止和反轉動畫。它需要一個 Ticker,在 vsync 發生時發出訊號,並在執行時在每一幀之間產生 0 到 1 的線性插值。然後,你可以建立一或多個 Animation 並將它們附加到控制器上。

例如,您可以使用 `CurvedAnimation` 來實現沿著插值曲線的動畫。從這個意義上說,控制器是動畫進度的“主”源,而 `CurvedAnimation` 計算替換控制器預設線性運動的曲線。像元件一樣,Flutter 中的動畫也透過組合工作。

構建 Widget 樹時,將 Animation 分配給 Widget 的動畫屬性(例如 FadeTransition 的 opacity),並告訴控制器開始動畫。

以下示例展示瞭如何編寫一個 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),
      ),
    );
  }
}

有關更多資訊,請參閱動畫與動作 Widgets動畫教程動畫概述

如何在螢幕上繪圖/渲染?

#

Xamarin.Forms 沒有內建直接在螢幕上繪圖的方法。如果需要繪製自定義影像,很多人會使用 SkiaSharp。在 Flutter 中,你可以直接訪問 Skia Canvas,並能輕鬆在螢幕上繪圖。

Flutter 有兩個類可以幫助你在畫布上繪圖:CustomPaintCustomPainter,後者實現了你用於在畫布上繪圖的演算法。

要了解如何在 Flutter 中實現簽名繪製器,請參閱 StackOverflow 上關於 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 的不透明度(Opacity)在哪裡設定?

#

在 Xamarin.Forms 上,所有 VisualElement 都有一個 Opacity 屬性。在 Flutter 中,你需要將 Widget 包裝在 Opacity Widget 中來實現。

如何構建自定義 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));
  }
}

然後像使用任何其他 Flutter Widget 一樣使用 CustomButton

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

如何在頁面之間導航?

#

在 Xamarin.Forms 中,NavigationPage 類提供了一種分層導航體驗,使用者可以在頁面之間前進和後退。

Flutter 有類似的實現,使用 NavigatorRoutesRoute 是應用 Page 的抽象,而 Navigator 是一個管理路由的 Widget

路由大致對應於 Page。導航器的工作方式類似於 Xamarin.Forms 的 NavigationPage,它可以根據你想要前往或返回某個檢視而 push()(推送)或 pop()(彈出)路由。

要在頁面之間導航,你有幾種選擇:

  • 指定路由名稱的 MapMaterialApp)。
  • 直接導航到路由(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 是管理應用路由的堆疊。將路由推送到堆疊可進入該路由。從堆疊彈出路由可返回上一頁。這是透過 await push() 返回的 Future 來完成的。

async/await 與 .NET 的實現非常相似,詳見 非同步 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 執行網路程式碼,而不會導致 UI 卡頓,讓 Dart 完成繁重的工作

dart
Future<void> loadData() async {
  final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final http.Response response = await http.get(dataURL);
  setState(() {
    data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
  });
}

當 awaited 的網路呼叫完成後,透過呼叫 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, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  Future<void> loadData() async {
    final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    final http.Response response = await http.get(dataURL);
    setState(() {
      data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
    });
  }

  Widget getRow(int index) {
    return Padding(
      padding: const EdgeInsets.all(10),
      child: Text('Row ${data[index]['title']}'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView.builder(
        itemCount: data.length,
        itemBuilder: (context, index) {
          return getRow(index);
        },
      ),
    );
  }
}

有關在後臺執行工作以及 Flutter 與 Android 差異的更多資訊,請參考下一節。

如何將任務移至後臺執行緒?

#

由於 Flutter 是單執行緒的且執行著一個事件迴圈,因此你不必擔心執行緒管理或產生後臺執行緒。這與 Xamarin.Forms 非常相似。如果你正在執行 I/O 密集型工作(如磁碟訪問或網路呼叫),則可以安全地使用 async/await,一切就緒。

另一方面,如果需要進行計算密集型工作,則應將其移動到 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) as List).cast<Map<String, Object?>>();
  });
}

這就是你通常進行網路或資料庫呼叫的方式,它們都是 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, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  bool get showLoadingDialog => data.isEmpty;

  Future<void> loadData() async {
    final ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    final SendPort sendPort = await receivePort.first as SendPort;
    final List<Map<String, dynamic>> msg = await sendReceive(
      sendPort,
      'https://jsonplaceholder.typicode.com/posts',
    );
    setState(() {
      data = msg;
    });
  }

  // The entry point for the isolate
  static Future<void> dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    final ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);
    await for (final dynamic msg in port) {
      final String url = msg[0] as String;
      final SendPort replyTo = msg[1] as SendPort;

      final Uri dataURL = Uri.parse(url);
      final http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
    }
  }

  Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
    final ReceivePort response = ReceivePort();
    port.send(<dynamic>[msg, response.sendPort]);
    return response.first as Future<List<Map<String, dynamic>>>;
  }

  Widget getBody() {
    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。在 Flutter 中,使用流行的 http 軟體包進行網路呼叫非常簡單。它抽象了許多你通常需要自己實現的底層網路操作,使網路呼叫變得簡單。

要使用 http 軟體包,請將其新增到 pubspec.yaml 的依賴項中:

yaml
dependencies:
  http: ^1.4.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) as List).cast<Map<String, Object?>>();
  });
}

如何顯示長時間執行任務的進度?

#

在 Xamarin.Forms 中,你通常會直接在 XAML 中建立一個載入指示器,或透過 AcrDialogs 等第三方外掛建立。

在 Flutter 中,使用 ProgressIndicator Widget。透過布林標誌控制何時渲染它來以程式設計方式顯示進度。在長時間執行的任務開始之前通知 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, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  bool get showLoadingDialog => data.isEmpty;

  Future<void> loadData() async {
    final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    final http.Response response = await http.get(dataURL);
    setState(() {
      data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
    });
  }

  Widget getBody() {
    if (showLoadingDialog) {
      return getProgressDialog();
    }
    return getListView();
  }

  Widget getProgressDialog() {
    return const Center(child: CircularProgressIndicator());
  }

  ListView getListView() {
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, index) {
        return getRow(index);
      },
    );
  }

  Widget getRow(int 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 應用只有資產(assets)。所有原本位於 Android Resources/drawable-* 資料夾中的資源,在 Flutter 中都放在 assets 資料夾中。

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 就會載入它們。

例如,要向 Flutter 專案新增名為 my_icon.png 的新影像資產,並將其放置在我們隨意建立的 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.png

你可以直接在 Image.asset Widget 中訪問你的影像:

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 軟體包,請在 app widget 上指定 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,因此它同時具備用於基礎 Widget 本地化值的 GlobalWidgetsLocalizations 和用於 Material Widget 本地化的 MaterialWidgetsLocalizations。如果你的應用使用 WidgetsApp,則不需要後者。請注意,這兩個委託包含“預設”值,如果你希望應用自身的本地化文字也能被本地化,則需要為你自己的應用提供一個或多個委託。

初始化時,WidgetsApp(或 MaterialApp)會為你建立一個 Localizations Widget,其中包含你指定的委託。裝置的當前區域設定始終可以從當前上下文的 Localizations Widget(以 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 中使用的外部依賴項。查詢 Flutter 軟體包的好地方是 pub.dev

應用生命週期

#

如何監聽應用生命週期事件?

#

在 Xamarin.Forms 中,你的 Application 包含 OnStartOnResumeOnSleep。在 Flutter 中,你可以透過掛鉤到 WidgetsBinding 觀察者並監聽 didChangeAppLifecycleState() 更改事件來監聽類似的生命週期事件。

可觀察的生命週期事件有

inactive

應用處於非活動狀態且未接收使用者輸入。此事件僅限 iOS。

paused

應用當前對使用者不可見,未響應使用者輸入,但在後臺執行。

resumed

應用可見並正在響應使用者輸入。

suspending

應用暫時掛起。此事件僅限 Android。

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

佈局

#

StackLayout 的等價物是什麼?

#

在 Xamarin.Forms 中,你可以建立具有水平或垂直 OrientationStackLayout。Flutter 有類似的方法,但你會使用 RowColumn Widget。

你會注意到這兩個程式碼示例除 RowColumn Widget 外完全相同。子元件是一樣的,這一特性可用於開發隨時間變化的豐富佈局。

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 來實現覆蓋其他 Widget 的 Widget。在 Flutter 中,你可以使用 Stack Widget 來完成此操作。

此示例建立了兩個重疊的圖示。

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 Widget。你只需用想要滾動的內容填充 Widget 即可。

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

如果你有許多想要包含在滾動檢視中的專案(即使型別不同),你可能想使用 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 中的 Widget 新增 GestureRecognizer?

#

在 Xamarin.Forms 中,Element 可能包含你可以附加的點選事件。許多元素還包含與此事件繫結的 Command。或者,你會使用 TapGestureRecognizer。在 Flutter 中有兩種非常相似的方法:

  1. 如果 Widget 支援事件檢測,則向其傳遞一個函式並在函式中處理。例如,ElevatedButton 有一個 onPressed 引數:

    dart
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
    
  2. 如果 Widget 不支援事件檢測,請將 Widget 包裝在 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,你可以監聽多種手勢,例如:

  • 點選
onTapDown

可能導致點選的指標已在特定位置接觸螢幕。

onTapUp

觸發點選的指標已在特定位置停止接觸螢幕。

onTap

發生了一次點選。

onTapCancel

先前觸發 onTapDown 的指標不會導致點選。

  • 雙擊
onDoubleTap

使用者在同一位置快速連續點選了兩次螢幕。

  • 長按
onLongPress

指標在同一位置長時間保持與螢幕接觸。

  • 垂直拖動
onVerticalDragStart

指標已接觸螢幕並可能開始垂直移動。

onVerticalDragUpdate

與螢幕接觸的指標在垂直方向上發生了移動。

onVerticalDragEnd

之前與螢幕接觸並垂直移動的指標不再與螢幕接觸,並且在停止接觸螢幕時以特定速度移動。

  • 水平拖動
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),
          ),
        ),
      ),
    );
  }
}

列表檢視(ListView)與介面卡

#

Flutter 中 ListView 的等價物是什麼?

#

Flutter 中 ListView 的等價物就是……ListView

在 Xamarin.Forms ListView 中,你建立一個 ViewCell 和可能的 DataTemplateSelector,並將其傳遞給 ListView,它會根據 DataTemplateSelectorViewCell 的返回值渲染每一行。但是,你通常必須確保啟用了單元格回收(Cell Recycling),否則會遇到記憶體問題和滾動速度變慢的情況。

由於 Flutter 的不可變 Widget 模式,你只需將 Widget 列表傳遞給 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 中,使用傳入 Widget 提供的觸控處理功能即可。

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 中更新列表即可。或者,你可以為 ItemSource 屬性分配一個新的 List

在 Flutter 中,情況有所不同。如果你在 setState() 方法中更新 Widget 列表,你會很快發現數據在視覺上沒有變化。這是因為呼叫 setState() 時,Flutter 渲染引擎會檢查 Widget 樹以檢視是否有任何變化。當它到達你的 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(iOS 僅用 fontname)將此字型名稱分配給 FontFamily 屬性。

在 Flutter 中,將字型檔案放在一個資料夾中,並在 pubspec.yaml 檔案中引用它,類似於匯入影像的方式。

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

然後將字型分配給您的 Text Widget

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 Widget 上自定義其他樣式元素。Text Widget 的 style 引數接受一個 TextStyle 物件,您可以在其中自定義許多引數,例如

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

表單輸入

#

如何獲取使用者輸入?

#

Xamarin.Forms element 允許你直接查詢 element 以確定其屬性的狀態,或者它是否繫結到 ViewModel 中的屬性。

在 Flutter 中檢索資訊由專門的 Widget 處理,這與你習慣的方式不同。如果你有 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),
      ),
    );
  }
}

你可以在 檢索文字欄位的值 中找到更多資訊和完整程式碼清單。

Entry 的 Placeholder 對應什麼?

#

在 Xamarin.Forms 中,一些 Elements 支援 Placeholder 屬性,你可以為其賦值。例如:

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

在 Flutter 中,你可以透過將 InputDecoration 物件新增到文字 Widget 的 decoration 建構函式引數中,輕鬆地為輸入顯示“提示”或佔位符文字。

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

如何顯示驗證錯誤?

#

使用 Xamarin.Forms 時,如果你希望提供驗證錯誤的視覺提示,則需要在有驗證錯誤的 Element 周圍建立新的屬性和 VisualElement

在 Flutter 中,你可以將 InputDecoration 物件傳遞給文字 Widget 的 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 提供了 平臺通道(platform channels),用於與託管 Flutter 檢視的 ViewControllerActivity 進行通訊和交換資料。平臺通道本質上是一種非同步訊息傳遞機制,連線了 Dart 程式碼與宿主 ViewControllerActivity 以及它所執行的 iOS 或 Android 框架。你可以使用平臺通道在原生端執行方法,或從裝置感測器檢索資料。

除了直接使用平臺通道外,你還可以使用各種預製的 外掛,它們為特定目標封裝了原生程式碼和 Dart 程式碼。例如,你可以使用外掛直接從 Flutter 訪問相機膠捲和裝置攝像頭,而無需編寫自己的整合。外掛可以在 Dart 和 Flutter 的開源軟體包倉庫 pub.dev 上找到。一些軟體包可能支援 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 中,你在頂層 Widget 中宣告主題。

要充分利用應用中的 Material Components,可以將頂層 Widget MaterialApp 宣告為應用程式的入口點。MaterialApp 是一個便利的 Widget,它包裝了實現 Material Design 的應用程式通常所需的一系列 Widget。它在 WidgetsApp 的基礎上添加了 Material 特定功能。

您還可以使用 WidgetsApp 作為您的應用 Widget,它提供了一些相同的功能,但不如 MaterialApp 豐富。

要自定義任何子元件的顏色和樣式,請將 ThemeData 物件傳遞給 MaterialApp Widget。例如,在以下程式碼中,種子顏色方案被設定為深紫色,文字選擇顏色被設定為紅色。

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

資料庫與本地儲存

#

如何訪問共享偏好設定(Shared Preferences)或 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 支援效能分析、檢查堆、檢查 widget 樹、日誌診斷、除錯、觀察已執行程式碼行、除錯記憶體洩漏和記憶體碎片。有關更多資訊,請檢視 DevTools 文件。

通知

#

如何設定推送通知?

#

在 Android 中,你使用 Firebase Cloud Messaging 為你的應用設定推送通知。

在 Flutter 中,使用 firebase_messaging 外掛訪問此功能。有關使用 Firebase Cloud Messaging API 的更多資訊,請參閱 firebase_messaging 外掛文件。