本文件旨在幫助希望利用現有知識使用 Flutter 構建移動應用的 Xamarin.Forms 開發者。如果您理解 Xamarin.Forms 框架的基礎知識,那麼您可以將本文件作為 Flutter 開發的快速入門。

您在構建 Flutter 應用時,Android 和 iOS 知識和技能至關重要,因為 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 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 部分的重新構建。確保僅在必要時,並且僅在 widget 樹中已改變的部分上呼叫 setState(),否則可能會導致 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 應用程式中使用的頁面型別。這些頁面將包含 Elements 來顯示各種控制元件。在 Xamarin.Forms 中,EntryButtonElement 的示例。

在 Flutter 中,幾乎所有東西都是一個 widget。一個 Page,在 Flutter 中稱為 Route,是一個 widget。按鈕、進度條和動畫控制器都是 widget。構建路由時,您會建立一個 widget 樹。

Flutter 包含 Material Components 庫。這些是實現了 Material Design 指南的 widget。Material Design 是一個靈活的設計系統,針對所有平臺進行了最佳化,包括 iOS。

但 Flutter 足夠靈活和富有表現力,可以實現任何設計語言。例如,在 iOS 上,您可以使用 Cupertino widget 來生成看起來像 Apple 的 iOS 設計語言的介面。

如何更新 widget?

#

在 Xamarin.Forms 中,每個 PageElement 都是一個有狀態類,它具有屬性和方法。您透過更新屬性來更新 Element,並且這會傳播到原生控制元件。

在 Flutter 中,Widget 是不可變的,您不能透過更改屬性直接更新它們,而是必須處理 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 提供的佈局。

如何從佈局中新增或移除 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 中,您可以使用 ViewExtensions 建立簡單的動畫,其中包括 FadeToTranslateTo 等方法。您將在檢視上使用這些方法來執行所需的動畫。

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

然後在程式碼隱藏或行為中,這將在 1 秒內淡入影像。

csharp
myImage.FadeTo(0, 1000);

在 Flutter 中,您透過將 widget 包裝在動畫 widget 中來使用動畫庫為 widget 設定動畫。使用 AnimationController,它是一個 Animation,可以暫停、查詢、停止和反轉動畫。它需要一個 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),
      ),
    );
  }
}

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

如何在螢幕上繪製/畫圖?

#

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 都具有不透明度。在 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() 路由,具體取決於您是想導航到某個檢視還是從某個檢視返回。

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

  • 指定路由名稱的 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 實現非常相似,並在非同步 UI 中有更詳細的解釋。

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

dart
Object? coordinates = await Navigator.of(context).pushNamed('/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?>>();
  });
}

一旦等待的網路呼叫完成,透過呼叫 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,一切就緒。

另一方面,如果您需要進行計算密集型工作以使 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) 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 更新其狀態,並在任務結束後將其隱藏。

在下面的示例中,build 函式分為三個不同的函式。如果 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 應用只有資產。所有 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 會將其拾取。

例如,要將名為 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.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 包,請在應用 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 中,您有一個包含 OnStartOnResumeOnSleepApplication。在 Flutter 中,您可以透過掛鉤 WidgetsBinding 觀察者並監聽 didChangeAppLifecycleState() 更改事件來監聽類似的生命週期事件。

可觀察的生命週期事件有

非活躍
應用程式處於非活動狀態,並且不接收使用者輸入。此事件僅限 iOS。
暫停
應用程式當前對使用者不可見,不響應使用者輸入,但在後臺執行。
恢復
應用程式可見並響應使用者輸入。
暫停中
應用程式暫時暫停。此事件僅限 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 來實現覆蓋其他 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'));
}

如果您有許多專案要包裝在滾動中,即使是不同 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 中向 widget 新增 GestureRecognizers?

#

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

列表檢視和介面卡

#

Flutter 中 ListView 的等效物是什麼?

#

Flutter 中 ListView 的等效物是……ListView

在 Xamarin.Forms ListView 中,您建立一個 ViewCell 和可能一個 DataTemplateSelector,並將其傳遞給 ListView,後者使用您的 DataTemplateSelectorViewCell 返回的內容渲染每一行。但是,您通常必須確保開啟單元格回收,否則您將遇到記憶體問題和緩慢的滾動速度。

由於 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 中更新列表即可。或者,您可以將新 List 分配給 ItemSource 屬性。

在 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.builder 而不是 ListView,它接受兩個關鍵引數:列表的初始長度和項構建器函式。

項構建器函式類似於 Android 介面卡中的 getView 函式;它接受一個位置,並返回您希望在該位置渲染的行。

最後,但最重要的是,請注意 onTap() 函式不再重新建立列表,而是向其中新增內容。

有關更多資訊,請參閱 您的第一個 Flutter 應用 Codelab。

處理文字

#

如何為文字 widget 設定自定義字型?

#

在 Xamarin.Forms 中,您必須在每個原生專案中新增自定義字型。然後,在您的 Element 中,您將使用 filename#fontname 將此字型名稱分配給 FontFamily 屬性,而 iOS 僅使用 fontname

在 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 中,您可以透過為文字 widget 的 decoration 建構函式引數新增一個 InputDecoration 物件,輕鬆地為您的輸入顯示“提示”或佔位符文字。

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

如何顯示驗證錯誤?

#

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

在 Flutter 中,您將一個 InputDecoration 物件傳遞給文字 widget 的裝飾建構函式。

但是,您不希望一開始就顯示錯誤。相反,當用戶輸入無效資料時,更新狀態,並傳遞一個新的 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 中,您在頂層 widget 中宣告主題。

為了充分利用應用程式中的 Material 元件,您可以將頂層 widget MaterialApp 宣告為應用程式的入口點。MaterialApp 是一個便利 widget,它封裝了 Material Design 應用程式通常需要的一些 widget。它在 WidgetsApp 的基礎上添加了 Material 特定的功能。

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

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

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 支援效能分析、檢查堆、檢查 widget 樹、日誌診斷、除錯、觀察已執行程式碼行、除錯記憶體洩漏和記憶體碎片。有關更多資訊,請檢視 DevTools 文件。

通知

#

如何設定推送通知?

#

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

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