內容

Flutter for Xamarin.Forms 開發人員

內容

這份文件是為 Xamarin.Forms 開發人員撰寫的,他們希望運用現有知識來建立使用 Flutter 的行動應用程式。如果您了解 Xamarin.Forms 架構的基本原理,那麼您可以使用這份文件作為 Flutter 開發的起點。

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

這份文件可用作食譜,您可以跳躍並找出與您的需求最相關的問題。

專案設定

應用程式如何啟動?

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

LoadApplication(new App());

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

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

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

public class App : Application
{
    public App()
    {
        MainPage = new ContentPage
        {
            Content = new Label
            {
                Text = "Hello World",
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center
            }
        };
    }
}

在 Flutter 中,「所有東西都是小工具」,甚至包括應用程式本身。以下範例顯示 MyApp,一個簡單的應用程式 Widget

class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text(
        'Hello World!',
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

如何建立一個頁面?

Xamarin.Forms 有許多類型的頁面;ContentPage 是最常見的。在 Flutter 中,您指定一個包含根頁面的應用程式小工具。您可以使用 MaterialApp 小工具,它支援 Material Design,或者您可以使用 CupertinoApp 小工具,它支援 iOS 風格的應用程式,或者您可以使用較低層級的 WidgetsApp,您可以隨意自訂。

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

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

class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

從這裡開始,你的實際第一頁是另一個 Widget,你可以在其中建立你的狀態。

一個有狀態的 Widget,例如以下的 MyHomePage,包含兩部分。第一部分本身是不可變的,它會建立一個 State 物件,用來儲存物件的狀態。這個 State 物件會持續存在於 Widget 的生命週期中。

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

這個 State 物件會實作有狀態 Widget 的 build() 方法。

當 Widget 樹的狀態改變時,呼叫 setState(),它會觸發 UI 的那一部分重新建構。務必只在必要時呼叫 setState(),而且只針對 Widget 樹中已變更的部分呼叫,否則可能會導致 UI 效能不佳。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set the appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

在 Flutter 中,UI(也稱為 Widget 樹)是不可變的,這表示一旦建構完成,你就無法變更其狀態。你可以變更 State 類別中的欄位,然後呼叫 setState() 來重新建構整個 Widget 樹。

這種產生 UI 的方式與 Xamarin.Forms 不同,但這種方法有很多好處。

檢視

Flutter 中的 Page 或 Element 相當於什麼?

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

在 Flutter 中,幾乎所有東西都是小工具。Page(在 Flutter 中稱為 Route)是一種小工具。按鈕、進度條和動畫控制器都是小工具。在建立路徑時,您會建立一個小工具樹。

Flutter 包含 Material Components 函式庫。這些是小工具,會實作 Material Design 指南。Material Design 是一個彈性設計系統,針對所有平台進行最佳化,包括 iOS。

不過 Flutter 夠彈性和有表現力,可以實作任何設計語言。例如,在 iOS 上,您可以使用 Cupertino 小工具 來產生看起來像 Apple 的 iOS 設計語言 的介面。

如何更新小工具?

在 Xamarin.Forms 中,每個 PageElement 都是一個有狀態類別,有屬性和方法。您可以透過更新屬性來更新 Element,然後這個更新會傳播到原生控制項。

在 Flutter 中,Widget 是不可變的,您無法直接透過變更屬性來更新它們,而是必須使用 Widget 的狀態。

這就是有狀態與無狀態 Widget 概念的由來。StatelessWidget 正如其名,就是沒有狀態資訊的 Widget。

StatelessWidgets 在您所描述的使用者介面部分不依賴於物件中設定資訊以外的任何事物時很有用。

例如,在 Xamarin.Forms 中,這類似於放置印有您標誌的 Image。標誌在執行期間不會變更,因此在 Flutter 中使用 StatelessWidget

如果您想在執行 HTTP 呼叫或使用者互動後根據接收的資料動態變更 UI,則必須使用 StatefulWidget 並告訴 Flutter 架構 Widget 的 State 已更新,以便它可以更新該 Widget。

此處要注意的重要事項是,在核心部分,無狀態和有狀態 Widget 的行為相同。它們會重建每個畫面,差別在於 StatefulWidgetState 物件,用於儲存狀態資料並在畫面之間復原它。

如果您有疑問,請務必記住這個規則:如果 Widget 變更(例如,因為使用者互動),它是有狀態的。但是,如果 Widget 對變更做出反應,包含它的父 Widget 仍然可以是無狀態的,如果它本身不會對變更做出反應。

以下範例顯示如何使用 StatelessWidget。常見的 StatelessWidgetText 小工具。如果您查看 Text 小工具的實作,會發現它是 StatelessWidget 的子類別。

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

正如您所見,Text 小工具沒有與之關聯的狀態資訊,它會呈現建構函式中傳遞的內容,而且僅此而已。

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

若要達成此目的,請將 Text 小工具包覆在 StatefulWidget 中,並在使用者按一下按鈕時更新它,如下面的範例所示

import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  /// Default placeholder text
  String textToShow = 'I Like Flutter';

  void _updateText() {
    setState(() {
      // Update the text
      textToShow = 'Flutter is Awesome!';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: const Icon(Icons.update),
      ),
    );
  }
}

我如何配置小工具?什麼是 XAML 檔案的等效項?

在 Xamarin.Forms 中,大多數開發人員會以 XAML 編寫配置,雖然有時會用 C# 編寫。在 Flutter 中,您會在程式碼中使用小工具樹撰寫配置。

以下範例顯示如何顯示具有填補的小工具

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

您可以在 小工具目錄 中檢視 Flutter 提供的配置。

我如何從配置中新增或移除元素?

在 Xamarin.Forms 中,您必須在程式碼中移除或新增 Element。這包括設定 Content 屬性,或是在清單的情況下呼叫 Add()Remove()

在 Flutter 中,由於小工具是不可變的,因此沒有直接的等效項。您可以改為傳遞一個函式給會傳回小工具的父項,並使用布林旗標控制該子項的建立。

下列範例顯示當使用者按一下 FloatingActionButton 時,如何在兩個小工具之間切換

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  /// Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return const Text('Toggle One');
    }
    return CupertinoButton(
      onPressed: () {},
      child: const Text('Toggle Two'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: Center(child: _getToggleChild()),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: const Icon(Icons.update),
      ),
    );
  }
}

如何為小工具加上動畫?

在 Xamarin.Forms 中,您可以使用包含 FadeToTranslateTo 等方法的 ViewExtensions 來建立簡單的動畫。您可以在檢視中使用這些方法來執行所需的動畫。

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

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

myImage.FadeTo(0, 1000);

在 Flutter 中,您可以使用動畫函式庫來為小工具加上動畫,方法是將小工具包裝在動畫小工具內。使用 AnimationController,這是一個 Animation<double>,可以暫停、尋找、停止和反轉動畫。它需要一個 Ticker,在 vsync 發生時發出訊號,並在執行時在 0 和 1 之間產生線性內插。然後,您建立一個或多個 Animation,並將它們附加到控制器。

例如,您可以使用 CurvedAnimation 來實作沿著內插曲線的動畫。在此意義上,控制器是動畫進度的「主要」來源,而 CurvedAnimation 則計算取代控制器預設線性動作的曲線。如同小工具,Flutter 中的動畫會搭配組合使用。

在建立小工具樹時,您會將 Animation 指定給小工具的動畫屬性,例如 FadeTransition 的不透明度,並指示控制器開始動畫。

以下範例顯示如何撰寫 FadeTransition,讓小工具在您按下 FloatingActionButton 時淡入標誌

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

如需更多資訊,請參閱 動畫和動作小工具動畫教學動畫概觀

如何繪製/在螢幕上塗鴉?

Xamarin.Forms 從未有內建的方式直接在螢幕上繪製。許多人會使用 SkiaSharp,如果他們需要繪製自訂圖像。在 Flutter 中,您可以直接存取 Skia 畫布,並輕鬆在螢幕上繪製。

Flutter 有兩個類別可協助您繪製畫布:CustomPaintCustomPainter,後者實作您的演算法以繪製畫布。

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

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

小工具的不透明度在哪裡?

在 Xamarin.Forms 中,所有 VisualElement 都有一個不透明度。在 Flutter 中,您需要將小工具包覆在 Opacity 小工具 中才能達成此目的。

如何建置自訂小工具?

在 Xamarin.Forms 中,您通常會對 VisualElement 進行子類別化,或使用現有的 VisualElement,以覆寫和實作可達成所需行為的方法。

在 Flutter 中,透過 組合 較小的元件(而非延伸它們)來建置自訂小工具。這有點類似於實作基於 Grid 的自訂控制項,並新增許多 VisualElement,同時擴充自訂邏輯。

例如,您如何建置一個在建構函式中取用標籤的 CustomButton?建立一個組合帶有標籤的 ElevatedButton 的 CustomButton,而非延伸 ElevatedButton

class CustomButton extends StatelessWidget {
  const CustomButton(this.label, {super.key});

  final String label;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: Text(label),
    );
  }
}

然後使用 CustomButton,就像使用任何其他 Flutter 小工具一樣

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

如何於各頁面之間導覽?

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

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

一個路由大致對應到一個 Page。Navigator 的運作方式類似於 Xamarin.Forms NavigationPage,在於它可以 push()pop() 路由,視您是要導覽至某個檢視,還是從某個檢視返回。

若要於各頁面之間導覽,您有幾個選項

  • 指定路由名稱的 Map。(MaterialApp
  • 直接導覽至某個路由。(WidgetsApp

以下範例建立一個 Map

void main() {
  runApp(
    MaterialApp(
      home: const MyAppHome(), // becomes the route named '/'
      routes: <String, WidgetBuilder>{
        '/a': (context) => const MyPage(title: 'page A'),
        '/b': (context) => const MyPage(title: 'page B'),
        '/c': (context) => const MyPage(title: 'page C'),
      },
    ),
  );
}

透過將其名稱推播至 Navigator 來導覽至某個路由。

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

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

async/await 與 .NET 實作非常類似,且在 非同步 UI 中有更詳細的說明。

例如,若要啟動 location 路由,讓使用者選擇其位置,您可能會執行下列動作

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

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

Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});

如何導覽至另一個應用程式?

在 Xamarin.Forms 中,若要將使用者傳送至另一個應用程式,請使用特定 URI 架構,並使用 Device.OpenUrl("mailto://")

若要在 Flutter 中實作此功能,請建立原生平台整合,或使用 現有的外掛程式,例如 url_launcher,以及 pub.dev 上的其他許多套件。

非同步 UI

Flutter 中的 Device.BeginOnMainThread() 等同於什麼?

Dart 具有單執行緒執行模式,支援 Isolate(在另一個執行緒上執行 Dart 程式碼的方法)、事件迴圈和非同步程式設計。除非您產生 Isolate,否則您的 Dart 程式碼會在主 UI 執行緒中執行,並由事件迴圈驅動。

Dart 的單執行緒模式並不表示您需要將所有內容作為會導致 UI 凍結的封鎖操作執行。與 Xamarin.Forms 非常類似,您需要讓 UI 執行緒保持空閒。您會使用 async/await 來執行任務,在這些任務中,您必須等待回應。

在 Flutter 中,使用 Dart 語言提供的非同步設施,也稱為 async/await,來執行非同步工作。這與 C# 非常類似,任何 Xamarin.Forms 開發人員都應該很容易使用。

例如,您可以使用 async/await 執行網路程式碼,而不會導致 UI 暫停,並讓 Dart 執行繁重的工作

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

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

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

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, dynamic>> data = <Map<String, dynamic>>[];

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

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

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

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

請參閱下一節以取得有關在背景中執行工作的更多資訊,以及 Flutter 與 Android 的不同之處。

如何將工作移至背景執行緒?

由於 Flutter 是單執行緒且執行事件迴圈,因此您不必擔心執行緒管理或產生背景執行緒。這與 Xamarin.Forms 非常類似。如果您正在執行 I/O 相依的工作,例如磁碟存取或網路呼叫,則可以安全地使用 async/await,而且您已設定好所有內容。

另一方面,如果您需要執行會讓 CPU 忙碌的計算密集型工作,您會希望將其移至 Isolate 以避免封鎖事件迴圈,就像您會將任何類型的從主執行緒中移出一樣。這類似於您透過 Xamarin.Forms 中的 Task.Run() 將項目移至不同執行緒的方式。

對於 I/O 相依的工作,請將函式宣告為 async 函式,並在函式內的長時間執行工作中 await

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

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

但是,有時您可能會處理大量資料,而您的 UI 會因此而暫停。在 Flutter 中,請使用 Isolate 來利用多個 CPU 核心執行長時間執行或計算密集的工作。

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

以下範例顯示如何透過簡單的 Isolate 將資料分享回主執行緒以更新 UI。

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

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

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

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

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

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

在此,dataLoader() 是在自己的獨立執行緒中執行的 Isolate。在 Isolate 中,您可以執行更密集的 CPU 處理(例如,剖析大型 JSON),或執行計算密集的數學運算,例如加密或訊號處理。

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

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, dynamic>> data = <Map<String, dynamic>>[];

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

  bool get showLoadingDialog => data.isEmpty;

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

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

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

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

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

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

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: getBody(),
    );
  }
}

我如何進行網路要求?

在 Xamarin.Forms 中,您會使用 HttpClient。當您使用熱門的 http 套件 時,在 Flutter 中進行網路呼叫很容易。這會抽象化許多您通常會自己實作的網路,讓進行網路呼叫變得簡單。

若要使用 http 套件,請將其新增至 pubspec.yaml 中的相依性

dependencies:
  http: ^1.1.0

若要進行網路請求,請在 async 函數 http.get() 上呼叫 await

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

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

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

在 Flutter 中,請使用 ProgressIndicator 小工具。透過控制布林旗標在何時呈現進度,以編程方式顯示進度。在長時間執行的任務開始前,指示 Flutter 更新其狀態,並在任務結束後將其隱藏。

在以下範例中,建置函數分成三個不同的函數。如果 showLoadingDialogtrue(當 widgets.length == 0 時),則呈現 ProgressIndicator。否則,使用從網路呼叫傳回的資料呈現 ListView

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, dynamic>> data = <Map<String, dynamic>>[];

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

  bool get showLoadingDialog => data.isEmpty;

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

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: getBody(),
    );
  }
}

專案結構和資源

我在哪裡儲存我的影像檔案?

Xamarin.Forms 沒有平台獨立的影像儲存方式,您必須將影像放置在 iOS xcasset 資料夾,或在 Android 中的各種 drawable 資料夾。

Android 和 iOS 將資源和資產視為不同的項目,而 Flutter 應用程式只有資產。所有會儲存在 Android 上 Resources/drawable-* 資料夾中的資源,都會放置在 Flutter 的資產資料夾中。

Flutter 遵循類似 iOS 的簡單密度格式。資產可能是 1.0x2.0x3.0x 或任何其他倍數。Flutter 沒有 dp,但有邏輯像素,基本上與與裝置無關的像素相同。Flutter 的 devicePixelRatio 表示單一邏輯像素中物理像素的比例。

等同於 Android 密度區段的是

Android 密度限定詞 Flutter 像素比例
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

資產位於任何任意資料夾中,Flutter 沒有預先定義的資料夾結構。您在 pubspec.yaml 檔案中宣告資產(含位置),Flutter 會將它們挑選出來。

例如,若要將稱為 my_icon.png 的新圖片資產新增至我們的 Flutter 專案,並決定將其儲存在我們任意稱為 images 的資料夾中,您會將基本圖片 (1.0x) 放入 images 資料夾中,並將所有其他變體放入使用適當比例倍數命名的子資料夾中

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下來,您需要在 pubspec.yaml 檔案中宣告這些圖片

assets:
 - images/my_icon.jpeg

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

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

或使用 AssetImage

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

更詳細的資訊可以在 新增資源和圖片 中找到。

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

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

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

您可以這樣存取您的字串

Text(Strings.welcomeMessage);

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

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

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

import 'package:flutter_localizations/flutter_localizations.dart';

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      localizationsDelegates: <LocalizationsDelegate<dynamic>>[
        // Add app-specific localization delegate[s] here
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: <Locale>[
        Locale('en', 'US'), // English
        Locale('he', 'IL'), // Hebrew
        // ... other locales the app supports
      ],
    );
  }
}

委派包含實際的在地化值,而 supportedLocales 定義應用程式支援哪些在地化。上述範例使用 MaterialApp,因此它同時具有 GlobalWidgetsLocalizations(用於基本小工具在地化值)和 MaterialWidgetsLocalizations(用於 Material 小工具在地化)。如果您為應用程式使用 WidgetsApp,則不需要後者。請注意,這兩個委派包含「預設」值,但如果您也想要在地化自己的應用程式的在地化副本,則需要提供一個或多個委派。

初始化時,WidgetsApp(或 MaterialApp)會建立一個 Localizations 小工具,並提供您指定的委派。裝置的目前在地化語言隨時可以從目前內容的 Localizations 小工具(以 Locale 物件的形式)存取,或使用 Window.locale 存取。

若要存取在地化資源,請使用 Localizations.of() 方法存取由特定委派提供的特定在地化類別。使用 intl_translation 套件將可翻譯的副本萃取到 arb 檔案,以便進行翻譯,並將其匯入應用程式中,以搭配 intl 使用。

有關 Flutter 中國際化和在地化的更多詳細資訊,請參閱 國際化指南,其中包含有和沒有 intl 套件的範例程式碼。

我的專案檔案在哪裡?

在 Xamarin.Forms 中,您將擁有 csproj 檔案。Flutter 中最接近的等效檔案是 pubspec.yaml,其中包含套件相依性和各種專案詳細資料。與 .NET Standard 類似,位於同一個目錄中的檔案被視為專案的一部分。

Nuget 的等效檔案是什麼?我如何新增相依性?

在 .NET 生態系中,原生 Xamarin 專案和 Xamarin.Forms 專案可以存取 Nuget 和內建套件管理系統。Flutter 應用程式包含原生 Android 應用程式、原生 iOS 應用程式和 Flutter 應用程式。

在 Android 中,您可以透過新增至 Gradle 建置指令碼來新增相依性。在 iOS 中,您可以透過新增至 Podfile 來新增相依性。

Flutter 使用 Dart 自有的建置系統和 Pub 套件管理員。這些工具會委派原生 Android 和 iOS 包裝應用程式的建置至各自的建置系統。

一般來說,使用 pubspec.yaml 來宣告在 Flutter 中使用的外部相依性。尋找 Flutter 套件的理想地點是 pub.dev

應用程式生命週期

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

在 Xamarin.Forms 中,您有一個包含 OnStartOnResumeOnSleepApplication。在 Flutter 中,您可以透過連接到 WidgetsBinding 觀察者並監聽 didChangeAppLifecycleState() 變更事件來監聽類似的生命週期事件。

可觀察的生命週期事件為

不活動
應用程式處於不活動狀態,且未收到使用者輸入。此事件僅限於 iOS。
暫停
應用程式目前對使用者不可見,不會回應使用者的輸入,但會在背景執行。
已恢復
應用程式可見且會回應使用者的輸入。
已暫停
應用程式已暫時暫停。此事件僅限於 Android。

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

配置

什麼是 StackLayout 的等效項?

在 Xamarin.Forms 中,您可以使用 Orientation 為水平或垂直的 StackLayout 來建立。Flutter 也有類似的做法,不過您會使用 RowColumn 小工具。

如果您注意到,除了 RowColumn 小工具之外,兩個程式碼範例完全相同。子項相同,而且可以利用此功能來開發豐富的配置,這些配置可以隨著時間推移而使用相同的子項進行變更。

@override
Widget build(BuildContext context) {
  return const Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
@override
Widget build(BuildContext context) {
  return const Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );

什麼是 Grid 的等效項?

最接近 Grid 的等效項會是 GridView。這比您在 Xamarin.Forms 中習慣的強大許多。GridView 在內容超過其可視空間時會自動捲動。

@override
Widget build(BuildContext context) {
  return GridView.count(
    // Create a grid with 2 columns. If you change the scrollDirection to
    // horizontal, this would produce 2 rows.
    crossAxisCount: 2,
    // Generate 100 widgets that display their index in the list.
    children: List<Widget>.generate(
      100,
      (index) {
        return Center(
          child: Text(
            'Item $index',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
        );
      },
    ),
  );
}

您可能在 Xamarin.Forms 中使用 Grid 來實作會覆蓋其他小工具的小工具。在 Flutter 中,您可以使用 Stack 小工具來達成此目的。

此範例會建立兩個會互相覆蓋的圖示。

@override
Widget build(BuildContext context) {
  return const Stack(
    children: <Widget>[
      Icon(
        Icons.add_box,
        size: 24,
        color: Colors.black,
      ),
      Positioned(
        left: 10,
        child: Icon(
          Icons.add_circle,
          size: 24,
          color: Colors.black,
        ),
      ),
    ],
  );
}

什麼是 ScrollView 的等效項?

在 Xamarin.Forms 中,ScrollView 會包覆 VisualElement,如果內容大於裝置螢幕,它就會捲動。

在 Flutter 中,最接近的對應元件是 SingleChildScrollView 小工具。您只要填入您想要捲動的內容至小工具中即可。

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

如果您有許多項目想要包覆在捲軸中,即使是不同類型的 Widget,您可能想要使用 ListView。這看起來似乎有點小題大作,但在 Flutter 中,這比 Xamarin.Forms ListView 更為最佳化且較不費力,後者是建立在特定平台的控制項上。

@override
Widget build(BuildContext context) {
  return ListView(
    children: const <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

我該如何處理 Flutter 中的橫向轉場?

橫向轉場可以透過在 AndroidManifest.xml 中設定 configChanges 屬性來自動處理。

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

手勢偵測和觸控事件處理

我該如何將 GestureRecognizers 加入 Flutter 中的小工具?

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

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

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

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

我該如何處理小工具上的其他手勢?

在 Xamarin.Forms 中,您會將 GestureRecognizer 加入 View。您通常會受限於 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizerSwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非您自己建置。

在 Flutter 中,使用 GestureDetector,您可以聆聽各種手勢,例如

  • 點選
onTapDown
可能會導致點選的指標已在特定位置接觸到螢幕。
onTapUp
觸發點選的指標已停止在特定位置接觸螢幕。
onTap
已發生點選。
onTapCancel
先前觸發 onTapDown 的指標不會導致點選。
  • 雙點
onDoubleTap
使用者在短時間內連續兩次點選螢幕的相同位置。
  • 長按
onLongPress
指標已在相同位置接觸螢幕很長一段時間。
  • 垂直拖曳
onVerticalDragStart
指標已接觸螢幕,並可能開始垂直移動。
onVerticalDragUpdate
接觸螢幕的指標已在垂直方向進一步移動。
onVerticalDragEnd
先前接觸螢幕並垂直移動的指標已不再接觸螢幕,且在停止接觸螢幕時以特定速度移動。
  • 水平拖曳
onHorizontalDragStart
指標已接觸螢幕,可能會開始水平移動。
onHorizontalDragUpdate
與螢幕接觸的指標已進一步向水平方向移動。
onHorizontalDragEnd
先前與螢幕接觸並水平移動的指標不再與螢幕接觸,且在停止接觸螢幕時以特定速度移動。

以下範例顯示一個在雙擊時旋轉 Flutter 標誌的 GestureDetector

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 採用不可變小工具模式,因此您可以傳遞小工具清單到 ListView,而 Flutter 會負責確保捲動快速且順暢。

import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatelessWidget {
  const SampleAppPage({super.key});

  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

我要如何知道哪個清單項目已被按一下?

在 Xamarin.Forms 中,ListView 有 ItemTapped 方法,用於找出哪個項目已被按一下。您可能已使用許多其他技術,例如檢查 SelectedItemEventToCommand 行為何時變更。

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

import 'dart:developer' as developer;
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => GestureDetector(
        onTap: () {
          developer.log('Row $index tapped');
        },
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Text('Row $index'),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

如何動態更新 ListView?

在 Xamarin.Forms 中,如果您將 ItemsSource 屬性繫結到 ObservableCollection,您只需在 ViewModel 中更新清單即可。或者,您可以將新的 List 指定給 ItemSource 屬性。

在 Flutter 中,運作方式略有不同。如果您在 setState() 方法中更新 ListView 內的 Widget 清單,您很快就會發現資料在視覺上沒有改變。這是因為當呼叫 setState() 時,Flutter 渲染引擎會查看 Widget 樹狀結構,以查看是否有任何變更。當它到達您的 ListView 時,它會執行 == 檢查,並確定兩個 ListView 相同。沒有任何變更,因此不需要更新。

要以簡單的方式更新 ListView,請在 setState() 內建立新的 List,並將資料從舊清單複製到新清單。雖然這種方法很簡單,但不建議用於大型資料集,如以下範例所示。

import 'dart:developer' as developer;
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = <Widget>[];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  Widget getRow(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets = List<Widget>.from(widgets);
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: widgets),
    );
  }
}

建立清單的建議、有效率且有效的方法是使用 ListView.Builder。當您有動態清單或資料量非常大的清單時,此方法非常棒。這基本上等同於 Android 上的 RecyclerView,它會自動為您回收清單元素

import 'dart:developer' as developer;
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  Widget getRow(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

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

建立 ListView.builder,而非建立 ListView,它採用兩個主要參數:清單的初始長度和項目建立函式。

項目建立函式類似於 Android 介接器中的 getView 函式;它會採用一個位置,並傳回您要在該位置呈現的列。

最後,但最重要的是,請注意 onTap() 函式不再重新建立清單,而是新增至清單。

如需更多資訊,請參閱 您的第一個 Flutter 應用程式 codelab。

使用文字

如何在文字小工具上設定自訂字型?

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

在 Flutter 中,將字型檔案放置在資料夾中,並在 pubspec.yaml 檔案中參照它,類似於您匯入圖片的方式。

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

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

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Sample App')),
    body: const Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何設定文字小工具的樣式?

除了字型之外,您還可以在 Text 小工具上自訂其他樣式元素。Text 小工具的樣式參數採用 TextStyle 物件,您可以在其中自訂許多參數,例如

  • 顏色
  • 裝飾
  • 裝飾顏色
  • 裝飾樣式
  • 字型家族
  • 字型大小
  • 字型樣式
  • 字重
  • 雜湊碼
  • 高度
  • 繼承
  • 字母間距
  • 文字基準線
  • 字詞間距

表單輸入

如何擷取使用者輸入?

Xamarin.Forms element 讓您可以直接查詢 element 以確定其屬性的狀態,或是否繫結到 ViewModel 中的屬性。

在 Flutter 中擷取資訊是由特定小工具處理,與您習慣的方式不同。如果您有 TextFieldTextFormField,您可以提供 TextEditingController 來擷取使用者輸入

import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  const MyForm({super.key});

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  /// Create a text controller and use it to retrieve the current value
  /// of the TextField.
  final TextEditingController myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Retrieve Text Input')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: TextField(controller: myController),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text that the user has typed into our text field.
        onPressed: () {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text that the user has entered using the
                // TextEditingController.
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: const Icon(Icons.text_fields),
      ),
    );
  }
}

您可以在 擷取文字欄位的數值 中找到更多資訊和完整的程式碼清單,來自 Flutter 食譜

輸入欄位中 Placeholder 的等效項目是什麼?

在 Xamarin.Forms 中,有些 Elements 支援 Placeholder 屬性,您可以為其指定值。例如

<Entry Placeholder="This is a hint">

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

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

如何顯示驗證錯誤?

使用 Xamarin.Forms 時,如果您想提供驗證錯誤的視覺提示,您需要建立新的屬性和 VisualElement,將具有驗證錯誤的 Element 包圍起來。

在 Flutter 中,您會傳遞一個 InputDecoration 物件至文字小工具的 decoration 建構函式。

不過,您不希望一開始就顯示錯誤。相反地,當使用者輸入無效資料時,請更新狀態,並傳遞一個新的 InputDecoration 物件。

import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  const SampleAppPage({super.key});

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String? _errorText;

  String? _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    const String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
        r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
        r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
    final RegExp regExp = RegExp(emailRegexp);
    return regExp.hasMatch(em);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: Center(
        child: TextField(
          onSubmitted: (text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(
            hintText: 'This is a hint',
            errorText: _getErrorText(),
          ),
        ),
      ),
    );
  }
}

Flutter 外掛程式

與硬體、第三方服務和平臺互動

如何與平臺以及平臺原生程式碼互動?

Flutter 沒有直接在底層平臺上執行程式碼;相反地,組成 Flutter 應用程式的 Dart 程式碼會在裝置上原生地執行,「避開」平臺提供的 SDK。舉例來說,當您在 Dart 中執行網路要求時,它會直接在 Dart 環境中執行。您不會使用在撰寫原生應用程式時通常會用到的 Android 或 iOS API。您的 Flutter 應用程式仍然會以檢視的形式,寄宿在原生應用程式的 ViewControllerActivity 中,但您無法直接存取它或原生架構。

這並不表示 Flutter 應用程式無法與那些原生 API 或任何您擁有的原生程式碼互動。Flutter 提供 平台頻道,用於與主控您的 Flutter 檢視的 ViewControllerActivity 通訊並交換資料。平台頻道基本上是一種非同步訊息機制,用於將 Dart 程式碼與主控 ViewControllerActivity 以及它執行的 iOS 或 Android 架構建立橋接。例如,您可以使用平台頻道來在原生端執行方法,或從裝置的感測器擷取一些資料。

除了直接使用平台頻道之外,您還可以利用各種預先製作的 外掛程式,它們封裝了原生程式碼和 Dart 程式碼以達成特定目標。例如,您可以使用外掛程式直接從 Flutter 存取相機膠卷和裝置相機,而不必撰寫您自己的整合。外掛程式可在 pub.dev(Dart 和 Flutter 的開放原始碼套件存放庫)中找到。有些套件可能支援 iOS、Android 或兩者的原生整合。

如果您在 pub.dev 上找不到符合您需求的外掛程式,您可以 撰寫您自己的外掛程式,並 在 pub.dev 上發佈

如何存取 GPS 感測器?

使用 geolocator 社群外掛程式。

如何存取相機?

camera 外掛程式很受歡迎,用於存取相機。

如何使用 Facebook 登入?

要使用 Facebook 登入,請使用 flutter_facebook_login 社群外掛程式。

如何使用 Firebase 功能?

大部分的 Firebase 功能都包含在 第一方外掛程式 中。這些外掛程式是由 Flutter 團隊維護的第一方整合。

您也可以在 pub.dev 上找到一些第三方 Firebase 外掛程式,涵蓋第一方外掛程式未直接涵蓋的領域。

如何建立自己的自訂原生整合?

如果 Flutter 或其社群外掛缺少特定平台的功能,你可以依照開發套件和外掛頁面建立自己的外掛。

簡而言之,Flutter 的外掛架構很像在 Android 中使用事件總線:發送訊息並讓接收器處理,然後將結果發送回給你。在這種情況下,接收器是在 Android 或 iOS 上的原生側執行的程式碼。

主題(樣式)

如何設定我的應用程式主題?

Flutter 內建 Material Design 的漂亮實作,可處理你通常會執行的許多樣式和主題設定需求。

Xamarin.Forms 有一個全域性的 ResourceDictionary,你可以在應用程式中分享樣式。或者,目前預覽版中支援主題。

在 Flutter 中,你可以在頂層小工具中宣告主題。

若要充分利用應用程式中的 Material Components,你可以宣告頂層小工具 MaterialApp 作為應用程式的進入點。 MaterialApp 是便利的小工具,它會包裝許多應用程式實作 Material Design 時通常需要的元件。它建立在 WidgetsApp 上,並新增特定於 Material 的功能。

您也可以使用 WidgetsApp 作為您的應用程式小工具,它提供了一些相同的功能,但不如 MaterialApp 豐富。

若要自訂任何子元件的顏色和樣式,請將 ThemeData 物件傳遞給 MaterialApp 小工具。例如,在下列程式碼中,種子的配色方案設定為 deepPurple,而文字選取顏色為紅色。

class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textSelectionTheme:
            const TextSelectionThemeData(selectionColor: Colors.red),
      ),
      home: const SampleAppPage(),
    );
  }
}

資料庫和本地儲存

如何存取共用偏好設定或 UserDefaults?

Xamarin.Forms 開發人員可能會熟悉 Xam.Plugins.Settings 外掛程式。

在 Flutter 中,使用 shared_preferences 外掛程式存取等效功能。此外掛程式包含 UserDefaults 和 Android 等效項 SharedPreferences 的功能。

如何在 Flutter 中存取 SQLite?

在 Xamarin.Forms 中,大多數應用程式會使用 sqlite-net-pcl 外掛程式來存取 SQLite 資料庫。

在 Flutter 中,在 macOS、Android 和 iOS 上,使用 sqflite 外掛程式存取此功能。

偵錯

在 Flutter 中,我可以使用哪些工具來偵錯我的應用程式?

使用 DevTools 套件來偵錯 Flutter 或 Dart 應用程式。

DevTools 包含支援剖析、檢查堆積、檢查小工具樹、記錄診斷、偵錯、觀察執行的程式碼行、偵錯記憶體外洩和記憶體碎片化。如需更多資訊,請參閱 DevTools 文件。

通知

如何設定推播通知?

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

在 Flutter 中,使用 firebase_messaging 外掛程式存取此功能。如需使用 Firebase Cloud Messaging API 的更多資訊,請參閱 firebase_messaging 外掛程式文件。