內容

適用於 React Native 開發人員的 Flutter

內容

這份文件是針對 React Native (RN) 開發人員,希望將他們現有的 RN 知識應用於使用 Flutter 建置行動應用程式。如果您了解 RN 架構的基本原理,那麼您可以使用這份文件作為學習 Flutter 開發的入門方式。

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

針對 JavaScript 開發人員的 Dart 簡介 (ES6)

與 React Native 類似,Flutter 使用反應式樣式檢視。然而,RN 編譯成原生小工具,而 Flutter 則編譯成原生程式碼。Flutter 控制螢幕上的每個像素,這避免了因需要 JavaScript 橋接而造成的效能問題。

Dart 是一種容易學習的語言,並提供下列功能

  • 提供一個開放原始碼、可擴充的程式語言,用於建置網路、伺服器和行動應用程式。
  • 提供一個物件導向、單一繼承的語言,使用 C 風格語法,AOT 編譯成原生程式碼。
  • 可選擇編譯成 JavaScript。
  • 支援介面和抽象類別。

以下是 JavaScript 和 Dart 之間的一些差異範例。

進入點

JavaScript 沒有預先定義的進入函式,您必須定義進入點。

// JavaScript
function startHere() {
  // Can be used as entry point
}

在 Dart 中,每個應用程式都必須有一個頂層 main() 函式,作為應用程式的進入點。

/// Dart
void main() {}

DartPad 中試用看看。

列印至主控台

要在 Dart 中列印至主控台,請使用 print()

// JavaScript
console.log('Hello world!');
/// Dart
print('Hello world!');

DartPad 中試用看看。

變數

Dart 是類型安全的,它使用靜態類型檢查和執行時期檢查的組合,以確保變數值始終與變數的靜態類型相符。雖然類型是強制性的,但由於 Dart 執行類型推論,因此某些類型註解是可選的。

建立和指定變數

在 JavaScript 中,變數無法被類型化。

Dart 中,變數必須明確指定類型,或類型系統必須自動推論適當的類型。

// JavaScript
let name = 'JavaScript';
/// Dart
/// Both variables are acceptable.
String name = 'dart'; // Explicitly typed as a [String].
var otherName = 'Dart'; // Inferred [String] type.

DartPad 中試用。

有關更多資訊,請參閱 Dart 的類型系統

預設值

在 JavaScript 中,未初始化的變數為 undefined

在 Dart 中,未初始化的變數的初始值為 null。由於數字在 Dart 中是物件,因此即使是具有數字類型的未初始化變數,其值也為 null

// JavaScript
let name; // == undefined
// Dart
var name; // == null; raises a linter warning
int? x; // == null

DartPad 中試用。

有關更多資訊,請參閱有關 變數 的文件。

檢查 Null 或零

在 JavaScript 中,使用 == 比較運算子時,值為 1 或任何非 Null 物件會被視為 true

// JavaScript
let myNull = null;
if (!myNull) {
  console.log('null is treated as false');
}
let zero = 0;
if (!zero) {
  console.log('0 is treated as false');
}

在 Dart 中,只有布林值 true 才會被視為 true。

/// Dart
var myNull;
var zero = 0;
if (zero == 0) {
  print('use "== 0" to check zero');
}

DartPad 中試用。

函數

Dart 和 JavaScript 函數通常很相似。主要的差異在於宣告。

// JavaScript
function fn() {
  return true;
}
/// Dart
/// You can explicitly define the return type.
bool fn() {
  return true;
}

DartPad 中試用看看。

如需更多資訊,請參閱 函數 文件。

非同步程式設計

Future

與 JavaScript 類似,Dart 支援單執行緒執行。在 JavaScript 中,Promise 物件代表非同步作業的最終完成(或失敗)及其結果值。

Dart 使用 Future 物件來處理這個問題。

// JavaScript
class Example {
  _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    return fetch(url)
      .then(response => response.json())
      .then(responseJson => {
        const ip = responseJson.origin;
        return ip;
      });
  }
}

function main() {
  const example = new Example();
  example
    ._getIPAddress()
    .then(ip => console.log(ip))
    .catch(error => console.error(error));
}

main();
// Dart
import 'dart:convert';

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

class Example {
  Future<String> _getIPAddress() {
    final url = Uri.https('httpbin.org', '/ip');
    return http.get(url).then((response) {
      final ip = jsonDecode(response.body)['origin'] as String;
      return ip;
    });
  }
}

void main() {
  final example = Example();
  example
      ._getIPAddress()
      .then((ip) => print(ip))
      .catchError((error) => print(error));
}

如需更多資訊,請參閱 Future 物件文件。

asyncawait

async 函數宣告定義非同步函數。

在 JavaScript 中,async 函數傳回 Promiseawait 營運子用於等待 Promise

// JavaScript
class Example {
  async function _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    const response = await fetch(url);
    const json = await response.json();
    const data = json.origin;
    return data;
  }
}

async function main() {
  const example = new Example();
  try {
    const ip = await example._getIPAddress();
    console.log(ip);
  } catch (error) {
    console.error(error);
  }
}

main();

在 Dart 中,async 函數傳回 Future,而函數主體會排程稍後執行。await 營運子用於等待 Future

// Dart
import 'dart:convert';

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

class Example {
  Future<String> _getIPAddress() async {
    final url = Uri.https('httpbin.org', '/ip');
    final response = await http.get(url);
    final ip = jsonDecode(response.body)['origin'] as String;
    return ip;
  }
}

/// An async function returns a `Future`.
/// It can also return `void`, unless you use
/// the `avoid_void_async` lint. In that case,
/// return `Future<void>`.
void main() async {
  final example = Example();
  try {
    final ip = await example._getIPAddress();
    print(ip);
  } catch (error) {
    print(error);
  }
}

如需更多資訊,請參閱 async 和 await 文件。

基礎知識

如何建立 Flutter 應用程式?

若要使用 React Native 建立應用程式,您會從命令列執行 create-react-native-app

$ create-react-native-app <projectname>

若要建立 Flutter 應用程式,請執行下列其中一項操作

  • 使用已安裝 Flutter 和 Dart 外掛的 IDE。
  • 從命令列使用 flutter create 命令。請確定 Flutter SDK 在您的 PATH 中。
$ flutter create <projectname>

如需更多資訊,請參閱 入門,其中會引導您建立按鈕點擊計數器應用程式。建立 Flutter 專案會建立執行 Android 和 iOS 裝置上範例應用程式所需的所有檔案。

如何執行我的應用程式?

在 React Native 中,您會從專案目錄執行 npm runyarn run

您可以使用幾種方式執行 Flutter 應用程式

  • 使用已安裝 Flutter 和 Dart 外掛的 IDE 中的「執行」選項。
  • 從專案根目錄使用 flutter run

您的應用程式會在連線裝置、iOS 模擬器或 Android 模擬器上執行。

如需更多資訊,請參閱 Flutter 入門文件。

如何匯入小工具?

在 React Native 中,您需要匯入每個所需的元件。

// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

在 Flutter 中,若要使用 Material Design 函式庫的小工具,請匯入 material.dart 套件。若要使用 iOS 風格小工具,請匯入 Cupertino 函式庫。若要使用更基本的小工具組,請匯入 Widgets 函式庫。或者,您可以撰寫自己的小工具函式庫並匯入該函式庫。

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:my_widgets/my_widgets.dart';

無論您匯入哪個小工具套件,Dart 只會擷取應用程式中使用的小工具。

如需更多資訊,請參閱 Flutter Widget 目錄

React Native 中的「Hello world!」應用程式在 Flutter 中的等效項是什麼?

在 React Native 中,HelloWorldApp 類別會延伸 React.Component 並透過傳回檢視元件來實作 render 方法。

// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

const App = () => {
  return (
    <View style={styles.container}>
      <Text>Hello world!</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

export default App;

在 Flutter 中,你可以使用核心小工具函式庫中的 CenterText 小工具,建立一個相同的「Hello world!」應用程式。Center 小工具會成為小工具樹的根目錄,並有一個子項,也就是 Text 小工具。

// Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

以下圖片顯示基本 Flutter「Hello world!」應用程式的 Android 和 iOS UI。

Hello world app on Android
Android
Hello world app on iOS
iOS

現在你已經看過最基本的 Flutter 應用程式,下一部分將說明如何利用 Flutter 豐富的小工具函式庫,建立一個現代化且精緻的應用程式。

如何使用小工具並將其巢狀,以形成小工具樹?

在 Flutter 中,幾乎所有東西都是小工具。

小工具是應用程式使用者介面的基本建構區塊。你可以將小工具組合成一個稱為小工具樹的階層。每個小工具都會巢狀在父小工具內,並繼承其父項的屬性。甚至應用程式物件本身也是一個小工具。沒有獨立的「應用程式」物件。相反地,根小工具會擔任此角色。

小工具可以定義

  • 結構元件,例如按鈕或選單
  • 風格元件,例如字型或配色方案
  • 配置方面,例如填補或對齊

以下範例顯示使用 Material 函式庫中的小工具的「Hello world!」應用程式。在此範例中,小工具樹狀結構會巢狀置於 MaterialApp 根小工具內。

// Flutter
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: Text('Hello world'),
        ),
      ),
    );
  }
}

以下圖片顯示使用 Material Design 小工具建置的「Hello world!」。您會免費獲得比基本「Hello world!」應用程式更多的功能。

Hello world app on Android
Android
Hello world app on iOS
iOS

撰寫應用程式時,您會使用兩種小工具類型:StatelessWidgetStatefulWidgetStatelessWidget 就像其名稱所述,是一個沒有狀態的小工具。StatelessWidget 建立後,外觀就不會改變。StatefulWidget 會根據接收到的資料或使用者輸入動態變更狀態。

無狀態小工具和有狀態小工具之間的重要差異在於,StatefulWidgetState 物件,用於儲存狀態資料並在樹狀結構重建時傳遞資料,因此資料不會遺失。

在簡單或基本的應用程式中,巢狀小工具很容易,但隨著程式碼庫變大且應用程式變得複雜,您應該將深度巢狀小工具拆分為傳回小工具或較小類別的函式。建立獨立的函式和小工具讓您可以在應用程式中重複使用元件。

如何建立可重複使用的元件?

在 React Native 中,您會定義一個類別來建立一個可重複使用的元件,然後使用 props 方法來設定或傳回所選元素的屬性和值。在以下範例中,CustomCard 類別被定義,然後在父類別中使用。

// React Native
const CustomCard = ({ index, onPress }) => {
  return (
    <View>
      <Text> Card {index} </Text>
      <Button
        title="Press"
        onPress={() => onPress(index)}
      />
    </View>
  );
};

// Usage
<CustomCard onPress={this.onPress} index={item.key} />

在 Flutter 中,定義一個類別來建立一個自訂小工具,然後重複使用該小工具。您也可以定義並呼叫一個函式,傳回一個可重複使用的元件,如以下範例中的 build 函式。

/// Flutter
class CustomCard extends StatelessWidget {
  const CustomCard({
    super.key,
    required this.index,
    required this.onPress,
  });

  final int index;
  final void Function() onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: <Widget>[
          Text('Card $index'),
          TextButton(
            onPressed: onPress,
            child: const Text('Press'),
          ),
        ],
      ),
    );
  }
}

class UseCard extends StatelessWidget {
  const UseCard({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    /// Usage
    return CustomCard(
      index: index,
      onPress: () {
        print('Card $index');
      },
    );
  }
}

在先前的範例中,CustomCard 類別的建構函式使用 Dart 的大括號語法 { } 來表示 命名參數

若要要求這些欄位,請從建構函式中移除大括號,或在建構函式中加入 required

以下螢幕截圖顯示可重複使用的 CustomCard 類別範例。

Custom cards on Android
Android
Custom cards on iOS
iOS

專案結構和資源

我在哪裡開始撰寫程式碼?

lib/main.dart 檔案開始。當您建立 Flutter 應用程式時,它會自動產生。

// Dart
void main() {
  print('Hello, this is the main function.');
}

在 Flutter 中,進入點檔案是 {project_name}/lib/main.dart,執行從 main 函式開始。

Flutter 應用程式中的檔案如何建構?

當您建立一個新的 Flutter 專案時,它會建構以下目錄結構。您可以在稍後自訂它,但這是您的起點。

┬
└ project_name
  ┬
  ├ android      - Contains Android-specific files.
  ├ build        - Stores iOS and Android build files.
  ├ ios          - Contains iOS-specific files.
  ├ lib          - Contains externally accessible Dart source files.
    ┬
    └ src        - Contains additional source files.
    └ main.dart  - The Flutter entry point and the start of a new app.
                   This is generated automatically when you create a Flutter
                    project.
                   It's where you start writing your Dart code.
  ├ test         - Contains automated test files.
  └ pubspec.yaml - Contains the metadata for the Flutter app.
                   This is equivalent to the package.json file in React Native.

我在哪裡放置我的資源和資產,以及如何使用它們?

Flutter 資源或資產是與您的應用程式一起打包和部署,且在執行階段可存取的檔案。Flutter 應用程式可以包含下列資產類型

  • 靜態資料,例如 JSON 檔案
  • 組態檔案
  • 圖示和影像 (JPEG、PNG、GIF、動畫 GIF、WebP、動畫 WebP、BMP 和 WBMP)

Flutter 使用位於專案根目錄的 pubspec.yaml 檔案,來識別應用程式所需的資產。

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

assets 小節指定應與應用程式一起包含的檔案。每個資產都由相對於 pubspec.yaml 檔案的明確路徑識別,其中包含資產檔案。宣告資產的順序並不重要。所使用的實際目錄(本例中為 assets)並不重要。但是,雖然資產可以放置在任何應用程式目錄中,但最佳做法是將它們放置在 assets 目錄中。

在建置過程中,Flutter 會將資產放入稱為「資產組合」的特殊封存檔中,應用程式會在執行階段從中讀取。當資產的路徑指定在 pubspec.yaml 的資產區段中時,建置程序會在相鄰的子目錄中尋找具有相同名稱的任何檔案。這些檔案也會與指定的資產一起包含在資產組合中。Flutter 在為您的應用程式選擇解析度適當的影像時,會使用資產變體。

在 React Native 中,您可以透過將影像檔案放置在原始碼目錄中並參照它來新增靜態影像。

<Image source={require('./my-icon.png')} />
// OR
<Image
  source={{
    url: 'http://reactnative.dev.org.tw/img/tiny_logo.png'
  }}
/>

在 Flutter 中,使用小工具的建置方法中的 Image.asset 建構函數,將靜態影像新增到您的應用程式。

Image.asset('assets/background.png');

如需更多資訊,請參閱 在 Flutter 中新增資產和影像

如何透過網路載入影像?

在 React Native 中,您會在 Image 元件的 source prop 中指定 uri,並在需要時提供大小。

在 Flutter 中,使用 Image.network 建構函數從 URL 中包含圖片。

Image.network('http://flutter.dev.org.tw/assets/images/docs/owl.jpg');

如何安裝套件和套件外掛程式?

Flutter 支援使用其他開發人員貢獻給 Flutter 和 Dart 生態系統的共用套件。這讓您能夠快速建置您的應用程式,而無需從頭開始開發所有內容。包含特定平台程式碼的套件稱為套件外掛程式。

在 React Native 中,您會使用 yarn add {package-name}npm install --save {package-name} 從命令列安裝套件。

在 Flutter 中,使用下列說明安裝套件

  1. 若要將 google_sign_in 套件新增為相依性,請執行 flutter pub add
$ flutter pub add google_sign_in
  1. 使用 flutter pub get 從命令列安裝套件。如果您使用 IDE,它通常會為您執行 flutter pub get,或可能會提示您執行。
  2. 如下所示,將套件匯入您的應用程式程式碼
import 'package:flutter/material.dart';

如需更多資訊,請參閱 使用套件開發套件和外掛程式

您可以在 Flutter 套件 區段中找到許多由 Flutter 開發人員共用的套件,此區段位於 pub.dev

Flutter 小工具

在 Flutter 中,您可以使用小工具來建立您的使用者介面,這些小工具會根據其目前的設定和狀態來描述其檢視應呈現的樣貌。

小工具通常由許多小型、單一用途的小工具組成,這些小工具會巢狀排列以產生強大的效果。例如,Container 小工具包含幾個負責配置、繪製、定位和調整大小的小工具。具體來說,Container 小工具包含 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 小工具。您無需透過子類化 Container 來產生自訂效果,而是可以透過新穎且獨特的方式來組成這些和其他簡單小工具。

Center 小工具是您可以控制配置的另一個範例。若要置中一個小工具,請將其包覆在 Center 小工具中,然後使用配置小工具來進行對齊、列、欄和格線。這些配置小工具沒有自己的視覺表示。相反地,它們的唯一目的是控制另一個小工具配置的某個面向。若要了解小工具為何會以特定方式呈現,通常有助於檢查鄰近的小工具。

如需更多資訊,請參閱 Flutter 技術概觀

如需瞭解更多關於 Widgets 套件中的核心小工具的資訊,請參閱 Flutter 基本小工具Flutter 小工具目錄Flutter 小工具索引

檢視

View 容器的等效項是什麼?

在 React Native 中,View 是支援使用 Flexbox 進行配置、樣式、觸控處理和無障礙控制的容器。

在 Flutter 中,你可以使用 Widgets 函式庫中的核心配置小工具,例如 ContainerColumnRowCenter。如需進一步瞭解,請參閱 配置小工具 目錄。

FlatListSectionList 的等效項是什麼?

List 是垂直排列元件的可捲動清單。

在 React Native 中,FlatListSectionList 用於呈現簡單或分段清單。

// React Native
<FlatList
  data={[ ... ]}
  renderItem={({ item }) => <Text>{item.key}</Text>}
/>

ListView 是 Flutter 中最常用的捲動小工具。預設建構函式會使用明確的子項清單。 ListView 最適合用於少數小工具。對於大型或無限清單,請使用 ListView.builder,它會依需求建立其子項,而且只會建立那些可見的子項。

var data = [
  'Hello',
  'World',
];
return ListView.builder(
  itemCount: data.length,
  itemBuilder: (context, index) {
    return Text(data[index]);
  },
);
Flat list on Android
Android
Flat list on iOS
iOS

若要瞭解如何實作無限捲動清單,請參閱官方 infinite_list 範例。

如何使用畫布進行繪圖或彩繪?

在 React Native 中,畫布元件不存在,因此會使用 react-native-canvas 等第三方函式庫。

// React Native
const CanvasComp = () => {
  const handleCanvas = (canvas) => {
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'skyblue';
    ctx.beginPath();
    ctx.arc(75, 75, 50, 0, 2 * Math.PI);
    ctx.fillRect(150, 100, 300, 300);
    ctx.stroke();
  };

  return (
    <View>
      <Canvas ref={this.handleCanvas} />
    </View>
  );
}

在 Flutter 中,您可以使用 CustomPaintCustomPainter 類別繪製到畫布。

下列範例顯示如何使用 CustomPaint 小工具在彩繪階段進行繪圖。它實作抽象類別 CustomPainter,並將其傳遞給 CustomPaint 的 painter 屬性。 CustomPaint 子類別必須實作 paint()shouldRepaint() 方法。

class MyCanvasPainter extends CustomPainter {
  const MyCanvasPainter();

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()..color = Colors.amber;
    canvas.drawCircle(const Offset(100, 200), 40, paint);
    final Paint paintRect = Paint()..color = Colors.lightBlue;
    final Rect rect = Rect.fromPoints(
      const Offset(150, 300),
      const Offset(300, 400),
    );
    canvas.drawRect(rect, paintRect);
  }

  @override
  bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
}

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: CustomPaint(painter: MyCanvasPainter()),
    );
  }
}
Canvas on Android
Android
Canvas on iOS
iOS

版面配置

如何使用小工具定義版面配置屬性?

在 React Native 中,大部分版面配置都可以透過傳遞給特定元件的道具來完成。例如,您可以在 View 元件上使用 style 道具,以指定彈性盒模型屬性。若要將元件排列成一欄,您可以指定下列道具: flexDirection: 'column'

// React Native
<View
  style={{
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}
>

在 Flutter 中,配置主要由專門設計用於提供配置的小工具定義,並結合控制小工具及其樣式屬性。

例如,ColumnRow 小工具會取得子項陣列,並分別將它們垂直和水平排列。Container 小工具會取得配置和樣式屬性的組合,而 Center 小工具會將其子小工具置中。

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        Container(
          color: Colors.red,
          width: 100,
          height: 100,
        ),
        Container(
          color: Colors.blue,
          width: 100,
          height: 100,
        ),
        Container(
          color: Colors.green,
          width: 100,
          height: 100,
        ),
      ],
    ),
  );

Flutter 在其核心小工具程式庫中提供各種配置小工具。例如,PaddingAlignStack

如需完整清單,請參閱 配置小工具

Layout on Android
Android
Layout on iOS
iOS

如何分層小工具?

在 React Native 中,可以使用 absolute 定位來分層元件。

Flutter 使用 Stack 小工具將子小工具分層排列。這些小工具可以完全或部分重疊基本小工具。

Stack 小工具會根據其方塊的邊緣來定位其子項。如果您只是想重疊多個子小工具,這個類別會很有用。

@override
Widget build(BuildContext context) {
  return Stack(
    alignment: const Alignment(0.6, 0.6),
    children: <Widget>[
      const CircleAvatar(
        backgroundImage: NetworkImage(
          'https://avatars3.githubusercontent.com/u/14101776?v=4',
        ),
      ),
      Container(
        color: Colors.black45,
        child: const Text('Flutter'),
      ),
    ],
  );

前一個範例使用 Stack 來疊加一個容器(在半透明黑色背景上顯示其 Text)在 CircleAvatar 上方。Stack 使用對齊屬性和 Alignment 座標來偏移文字。

Stack on Android
Android
Stack on iOS
iOS

更多資訊,請參閱 Stack 類別文件。

樣式

如何設定元件的樣式?

在 React Native 中,內聯樣式和 stylesheets.create 用於設定元件的樣式。

// React Native
<View style={styles.container}>
  <Text style={{ fontSize: 32, color: 'cyan', fontWeight: '600' }}>
    This is a sample text
  </Text>
</View>

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

在 Flutter 中,Text 小工具可以為其樣式屬性採用 TextStyle 類別。如果您想在多個地方使用相同的文字樣式,您可以建立一個 TextStyle 類別,並將其用於多個 Text 小工具。

const TextStyle textStyle = TextStyle(
  color: Colors.cyan,
  fontSize: 32,
  fontWeight: FontWeight.w600,
);

return const Center(
  child: Column(
    children: <Widget>[
      Text('Sample text', style: textStyle),
      Padding(
        padding: EdgeInsets.all(20),
        child: Icon(
          Icons.lightbulb_outline,
          size: 48,
          color: Colors.redAccent,
        ),
      ),
    ],
  ),
);
Styling on Android
Android
Styling on iOS
iOS

如何使用 IconsColors

React Native 不支援圖示,因此使用第三方程式庫。

在 Flutter 中,匯入 Material 程式庫也會拉入豐富的 Material 圖示色彩

return const Icon(Icons.lightbulb_outline, color: Colors.redAccent);

使用 Icons 類別時,請務必在專案的 pubspec.yaml 檔案中設定 uses-material-design: true。這可確保顯示圖示的 MaterialIcons 字型包含在您的應用程式中。一般來說,如果您打算使用 Material 程式庫,您應該包含這行。

name: my_awesome_application
flutter:
  uses-material-design: true

Flutter 的 Cupertino(iOS 風格) 套件提供高保真小工具,適用於目前的 iOS 設計語言。若要使用 CupertinoIcons 字型,請在專案的 pubspec.yaml 檔案中新增 cupertino_icons 的依賴項。

name: my_awesome_application
dependencies:
  cupertino_icons: ^1.0.6

若要針對元件的色彩和樣式進行全域自訂,請使用 ThemeData 指定主題各方面預設的色彩。將 MaterialApp 中的主題屬性設定為 ThemeData 物件。Colors 類別提供 Material Design 色彩盤 中的色彩。

下列範例將色彩配置從種子設定為 deepPurple,並將文字選取設定為 red

class SampleApp extends StatelessWidget {
  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(),
    );
  }
}

如何新增樣式主題?

在 React Native 中,常見的主題會在樣式表中針對元件定義,然後在元件中使用。

在 Flutter 中,透過在 ThemeData 類別中定義樣式,並將其傳遞給 MaterialApp 小工具中的主題屬性,即可為幾乎所有項目建立統一的樣式。

@override
Widget build(BuildContext context) {
  return MaterialApp(
    theme: ThemeData(
      primaryColor: Colors.cyan,
      brightness: Brightness.dark,
    ),
    home: const StylingPage(),
  );
}

即使不使用 MaterialApp 小工具,也可以套用 ThemeTheme 小工具會在 data 參數中採用 ThemeData,並將 ThemeData 套用至其所有子小工具。

@override
Widget build(BuildContext context) {
  return Theme(
    data: ThemeData(
      primaryColor: Colors.cyan,
      brightness: brightness,
    ),
    child: Scaffold(
      backgroundColor: Theme.of(context).primaryColor,
      //...
    ),
  );
}

狀態管理

狀態是在小工具建置時可以同步讀取的資訊,或是小工具生命週期中可能會變更的資訊。若要在 Flutter 中管理應用程式狀態,請使用 StatefulWidget 搭配 State 物件。

如需瞭解在 Flutter 中管理狀態的方式,請參閱 狀態管理

StatelessWidget

Flutter 中的 StatelessWidget 是不需要狀態變更的小工具,它沒有要管理的內部狀態。

當您描述的使用者介面部分僅依賴物件本身的組態資訊和膨脹小工具的 BuildContext 時,無狀態小工具會很有用。

AboutDialogCircleAvatarText 是繼承 StatelessWidget 的無狀態小工具範例。

import 'package:flutter/material.dart';

void main() => runApp(
      const MyStatelessWidget(
        text: 'StatelessWidget Example to show immutable data',
      ),
    );

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({
    super.key,
    required this.text,
  });

  final String text;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        text,
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

前一個範例使用 MyStatelessWidget 類別的建構函式傳遞標示為 finaltext。此類別延伸 StatelessWidget,它包含不可變資料。

無狀態小工具的 build 方法通常只在三種情況下呼叫

  • 當小工具插入樹狀結構時
  • 當小工具的父層變更其組態時
  • 當它所依賴的 InheritedWidget 變更時

StatefulWidget

一個 StatefulWidget 是一個會變更狀態的小工具。使用 setState 方法來管理 StatefulWidget 的狀態變更。呼叫 setState() 會告訴 Flutter 架構狀態中有些東西已經變更,這會導致應用程式重新執行 build() 方法,以便應用程式可以反映變更。

狀態 是在建立小工具時可以同步讀取的資訊,並且可能會在小工具的生命週期中變更。小工具實作者有責任確保在狀態變更時立即通知狀態物件。當小工具可以動態變更時,請使用 StatefulWidget。例如,小工具的狀態會因在表單中輸入內容或移動滑桿而變更。或者,它可能會隨著時間而變更,例如資料饋送更新使用者介面。

CheckboxRadioSliderInkWellFormTextField 是從 StatefulWidget 繼承的狀態小工具範例。

以下範例宣告一個需要 createState() 方法的 StatefulWidget。此方法會建立管理小工具狀態的狀態物件 _MyStatefulWidgetState

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

  final String title;

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

以下狀態類別 _MyStatefulWidgetState 為小工具實作 build() 方法。當狀態變更時,例如使用者切換按鈕時,setState() 會以新的切換值呼叫。這會造成架構在 UI 中重新建立此小工具。

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  bool showText = true;
  bool toggleState = true;
  Timer? t2;

  void toggleBlinkState() {
    setState(() {
      toggleState = !toggleState;
    });
    if (!toggleState) {
      t2 = Timer.periodic(const Duration(milliseconds: 1000), (t) {
        toggleShowText();
      });
    } else {
      t2?.cancel();
    }
  }

  void toggleShowText() {
    setState(() {
      showText = !showText;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            if (showText)
              const Text(
                'This execution will be done before you can blink.',
              ),
            Padding(
              padding: const EdgeInsets.only(top: 70),
              child: ElevatedButton(
                onPressed: toggleBlinkState,
                child: toggleState
                    ? const Text('Blink')
                    : const Text('Stop Blinking'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

StatefulWidget 和 StatelessWidget 的最佳實務是什麼?

以下是設計小工具時需要考慮的幾件事。

  1. 判斷小工具應該是 StatefulWidget 還是 StatelessWidget

在 Flutter 中,小工具會是 Stateful 或 Stateless,視其是否依賴於狀態變更而定。

  • 如果小工具變更(使用者與其互動或資料饋送中斷 UI),則其為 Stateful
  • 如果小工具是最終的或不可變的,則其為 Stateless
  1. 判斷哪個物件管理小工具的狀態(對於 StatefulWidget)。

在 Flutter 中,有三個主要方法來管理狀態

  • 小工具管理自己的狀態
  • 父小工具管理小工具的狀態
  • 混合搭配方法

在決定要使用哪種方法時,請考慮以下原則

  • 如果相關狀態是使用者資料,例如核取方塊的已勾選或未勾選模式,或滑塊的位置,則狀態最適合由父小工具管理。
  • 如果相關狀態是美學的,例如動畫,則小工具本身最能管理狀態。
  • 有疑問時,讓父小工具管理子小工具的狀態。
  1. 建立 StatefulWidget 和 State 的子類別。

MyStatefulWidget 類別管理其自己的狀態,它延伸 StatefulWidget,它覆寫 createState() 方法以建立 State 物件,而架構會呼叫 createState() 來建立小工具。在此範例中,createState() 會建立 _MyStatefulWidgetState 的執行個體,這會在下一項最佳實務中實作。

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

  final String title;
  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    //...
  }
}
  1. 將 StatefulWidget 加入小工具樹。

在應用程式的建置方法中,將自訂的 StatefulWidget 加入小工具樹。

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyStatefulWidget(title: 'State Change Demo'),
    );
  }
}
State change on Android
Android
State change on iOS
iOS

屬性

在 React Native 中,大多數元件在建立時都可以透過不同的參數或屬性(稱為 props)進行自訂。這些參數可以使用 this.props 在子元件中使用。

// React Native
const CustomCard = ({ index, onPress }) => {
  return (
    <View>
      <Text> Card {index} </Text>
      <Button
        title='Press'
        onPress={() => onPress(index)}
      />
    </View>
  );
};

const App = () => {
  const onPress = (index) => {
    console.log('Card ', index);
  };

  return (
    <View>
      <FlatList
        data={[ /* ... */ ]}
        renderItem={({ item }) => (
          <CustomCard onPress={onPress} index={item.key} />
        )}
      />
    </View>
  );
};

在 Flutter 中,您可以將標記為 final 的區域變數或函式指定給在參數化建構函式中接收到的屬性。

/// Flutter
class CustomCard extends StatelessWidget {
  const CustomCard({
    super.key,
    required this.index,
    required this.onPress,
  });

  final int index;
  final void Function() onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: <Widget>[
          Text('Card $index'),
          TextButton(
            onPressed: onPress,
            child: const Text('Press'),
          ),
        ],
      ),
    );
  }
}

class UseCard extends StatelessWidget {
  const UseCard({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    /// Usage
    return CustomCard(
      index: index,
      onPress: () {
        print('Card $index');
      },
    );
  }
}
Cards on Android
Android
Cards on iOS
iOS

本機儲存

如果您不需要儲存大量資料,而且不需要結構,您可以使用 shared_preferences,它允許您讀取和寫入基本資料類型的持久性鍵值對:布林值、浮點數、整數、長整數和字串。

我如何儲存應用程式全域的持久性鍵值對?

在 React Native 中,您可以使用 AsyncStorage 組件的 setItemgetItem 函式儲存和擷取持續且全域性的應用程式資料。

// React Native
const [counter, setCounter] = useState(0)
...
await AsyncStorage.setItem( 'counterkey', json.stringify(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
  if (value != null) {
    setCounter(value);
  }
});

在 Flutter 中,使用 shared_preferences 外掛程式儲存和擷取持續且全域性的應用程式關鍵值資料。 shared_preferences 外掛程式包裝 iOS 上的 NSUserDefaults 和 Android 上的 SharedPreferences,提供簡單資料的持續儲存。

若要將 shared_preferences 套件新增為相依性,請執行 flutter pub add

$ flutter pub add shared_preferences
import 'package:shared_preferences/shared_preferences.dart';

若要實作持續資料,請使用 SharedPreferences 類別提供的設定器方法。設定器方法可供各種基本類型使用,例如 setIntsetBoolsetString。若要讀取資料,請使用 SharedPreferences 類別提供的適當取得器方法。每個設定器都有對應的取得器方法,例如 getIntgetBoolgetString

Future<void> updateCounter() async {
  final prefs = await SharedPreferences.getInstance();
  int? counter = prefs.getInt('counter');
  if (counter is int) {
    await prefs.setInt('counter', ++counter);
  }
  setState(() {
    _counter = counter;
  });
}

路由

大多數應用程式包含數個畫面,用於顯示不同類型的資訊。例如,您可能有一個產品畫面,用於顯示圖片,使用者可以輕觸產品圖片,在新的畫面中取得更多關於產品的資訊。

在 Android 中,新的畫面是新的 Activities。在 iOS 中,新的畫面是新的 ViewControllers。在 Flutter 中,畫面只是 Widgets!而且要在 Flutter 中導覽到新畫面,請使用 Navigator 小工具。

我如何導覽畫面之間?

在 React Native 中,有三個主要的導覽器:StackNavigator、分頁導覽器和抽屜導覽器。每個導覽器提供設定和定義畫面的方法。

// React Native
const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);
const SimpleApp = StackNavigator({
  Home: { screen: MyApp },
  stackScreen: { screen: StackScreen }
});
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,有兩個主要的小工具用於在畫面之間導覽

  • Route 是應用程式畫面或頁面的抽象。
  • Navigator 是管理路由的小工具。

Navigator 定義為管理一組子小工具,並具有堆疊原則的小工具。導覽器管理Route 物件的堆疊,並提供管理堆疊的方法,例如 Navigator.pushNavigator.pop。可以在 MaterialApp 小工具中指定路由清單,或者可以動態建立路由,例如在英雄動畫中。下列範例在 MaterialApp 小工具中指定命名路由。

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //...
      routes: <String, WidgetBuilder>{
        '/a': (context) => const UsualNavScreen(),
        '/b': (context) => const DrawerNavScreen(),
      },
      //...
    );
  }
}

若要導覽至命名路由,Navigator.of() 方法用於指定 BuildContext(小工具樹中某個小工具位置的控制項)。路由名稱傳遞給 pushNamed 函式,以導覽至指定的路由。

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

您也可以使用 Navigator 的 push 方法,將指定的 Route 新增至最緊密包圍指定 BuildContext 的導覽器歷程記錄,並轉換至該路由。在以下範例中,MaterialPageRoute 小工具是一個模式路由,會使用平台適應性轉場取代整個畫面。它將 WidgetBuilder 視為必要參數。

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const UsualNavScreen(),
  ),
);

如何使用標籤導覽和抽屜導覽?

在 Material Design 應用程式中,Flutter 導覽有兩個主要選項:標籤和抽屜。當空間不足以支援標籤時,抽屜提供了一個不錯的替代方案。

標籤導覽

在 React Native 中,createBottomTabNavigatorTabNavigation 用於顯示標籤和標籤導覽。

// React Native
import { createBottomTabNavigator } from 'react-navigation';

const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);

Flutter 提供了幾個專門用於抽屜和標籤導覽的小工具

TabController
TabBarTabBarView 之間協調標籤選取。
TabBar
顯示標籤的水平列。
Tab
建立一個 material design TabBar 標籤。
TabBarView
顯示與目前選取標籤對應的小工具。
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);

  @override
  Widget build(BuildContext context) {
    return TabBar(
      controller: controller,
      tabs: const <Tab>[
        Tab(icon: Icon(Icons.person)),
        Tab(icon: Icon(Icons.email)),
      ],
    );
  }
}

需要一個 TabController 來協調 TabBarTabBarView 之間的標籤選取。 TabController 建構函式的 length 參數是標籤的總數。需要一個 TickerProvider 來觸發通知,只要一個畫面觸發狀態變更。 TickerProvidervsync。只要建立一個新的 TabController,就將 vsync: this 參數傳遞給 TabController 建構函式。

TickerProvider 是一個介面,由可以販賣 Ticker 物件的類別實作。任何必須在觸發畫面時收到通知的物件都可以使用 Ticker,但最常見的是透過 AnimationController 間接使用。 AnimationController 需要 TickerProvider 來取得其 Ticker。如果您要從 State 建立 AnimationController,則可以使用 TickerProviderStateMixinSingleTickerProviderStateMixin 類別來取得合適的 TickerProvider

Scaffold 小工具包覆一個新的 TabBar 小工具,並建立兩個分頁。 TabBarView 小工具傳遞為 Scaffold 小工具的 body 參數。對應到 TabBar 小工具的分頁的所有畫面都是 TabBarView 小工具的子項,以及相同的 TabController

class _NavigationHomePageState extends State<NavigationHomePage>
    with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: Material(
          color: Colors.blue,
          child: TabBar(
            tabs: const <Tab>[
              Tab(
                icon: Icon(Icons.person),
              ),
              Tab(
                icon: Icon(Icons.email),
              ),
            ],
            controller: controller,
          ),
        ),
        body: TabBarView(
          controller: controller,
          children: const <Widget>[HomeScreen(), TabScreen()],
        ));
  }
}

抽屜導覽

在 React Native 中,匯入需要的 react-navigation 套件,然後使用 createDrawerNavigatorDrawerNavigation

// React Native
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,我們可以使用 Drawer 小工具與 Scaffold 結合使用,以建立具有 Material Design 抽屜的版面。若要將 Drawer 加入應用程式,請將其包覆在 Scaffold 小工具中。 Scaffold 小工具為遵循 Material Design 指南的應用程式提供一致的視覺結構。它也支援特殊 Material Design 元件,例如 DrawersAppBarsSnackBars

Drawer 小工具是一個 Material Design 面板,它會從 Scaffold 的邊緣水平滑入,以在應用程式中顯示導覽連結。您可以提供 ElevatedButtonText 小工具,或一串項目清單,以顯示為 Drawer 小工具的子項。在以下範例中,ListTile 小工具提供點選導覽。

@override
Widget build(BuildContext context) {
  return Drawer(
    elevation: 20,
    child: ListTile(
      leading: const Icon(Icons.change_history),
      title: const Text('Screen2'),
      onTap: () {
        Navigator.of(context).pushNamed('/b');
      },
    ),
  );
}

Scaffold 小工具也包含一個 AppBar 小工具,當 Drawer 可用於 Scaffold 時,它會自動顯示適當的 IconButton 以顯示 DrawerScaffold 會自動處理邊緣滑動手勢以顯示 Drawer

@override
Widget build(BuildContext context) {
  return Scaffold(
    drawer: Drawer(
      elevation: 20,
      child: ListTile(
        leading: const Icon(Icons.change_history),
        title: const Text('Screen2'),
        onTap: () {
          Navigator.of(context).pushNamed('/b');
        },
      ),
    ),
    appBar: AppBar(title: const Text('Home')),
    body: Container(),
  );
}
Navigation on Android
Android
Navigation on iOS
iOS

手勢偵測與觸控事件處理

Flutter 支援點擊、拖曳和縮放,以偵測手勢並做出回應。Flutter 中的手勢系統有兩個獨立的層級。第一個層級包含原始指標事件,描述指標(例如觸控、滑鼠和觸控筆移動)在螢幕上的位置和移動。第二個層級包含手勢,描述由一個或多個指標移動組成的語義動作。

如何將點擊或按壓監聽器新增至小工具?

在 React Native 中,使用 PanResponderTouchable 元件將監聽器新增至元件。

// React Native
<TouchableOpacity
  onPress={() => {
    console.log('Press');
  }}
  onLongPress={() => {
    console.log('Long Press');
  }}
>
  <Text>Tap or Long Press</Text>
</TouchableOpacity>

對於更複雜的手勢和將多個觸控結合為單一的手勢,請使用 PanResponder

// React Native
const App = () => {
  const panResponderRef = useRef(null);

  useEffect(() => {
    panResponderRef.current = PanResponder.create({
      onMoveShouldSetPanResponder: (event, gestureState) =>
        !!getDirection(gestureState),
      onPanResponderMove: (event, gestureState) => true,
      onPanResponderRelease: (event, gestureState) => {
        const drag = getDirection(gestureState);
      },
      onPanResponderTerminationRequest: (event, gestureState) => true
    });
  }, []);

  return (
    <View style={styles.container} {...panResponderRef.current.panHandlers}>
      <View style={styles.center}>
        <Text>Swipe Horizontally or Vertically</Text>
      </View>
    </View>
  );
};

在 Flutter 中,若要將點擊(或按壓)監聽器新增至小工具,請使用按鈕或具有 onPress: field 的可觸控小工具。或者,透過將小工具包覆在 GestureDetector 中,為任何小工具新增手勢偵測。

@override
Widget build(BuildContext context) {
  return GestureDetector(
    child: Scaffold(
      appBar: AppBar(title: const Text('Gestures')),
      body: const Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Tap, Long Press, Swipe Horizontally or Vertically'),
        ],
      )),
    ),
    onTap: () {
      print('Tapped');
    },
    onLongPress: () {
      print('Long Pressed');
    },
    onVerticalDragEnd: (value) {
      print('Swiped Vertically');
    },
    onHorizontalDragEnd: (value) {
      print('Swiped Horizontally');
    },
  );
}

如需更多資訊,包括 Flutter GestureDetector 回呼的清單,請參閱 GestureDetector 類別

Gestures on Android
Android
Gestures on iOS
iOS

建立 HTTP 網路要求

對大多數應用程式而言,從網際網路擷取資料是很常見的。在 Flutter 中,http 套件提供從網際網路擷取資料最簡單的方法。

如何從 API 呼叫擷取資料?

React Native 提供 Fetch API 用於網路連線,您可以提出擷取要求,然後接收回應以取得資料。

// React Native
const [ipAddress, setIpAddress] = useState('')

const _getIPAddress = () => {
  fetch('https://httpbin.org/ip')
    .then(response => response.json())
    .then(responseJson => {
      setIpAddress(responseJson.origin);
    })
    .catch(error => {
      console.error(error);
    });
};

Flutter 使用 http 套件。

若要將 http 套件新增為相依性,請執行 flutter pub add

$ flutter pub add http

Flutter 使用 dart:io 核心 HTTP 支援用戶端。若要建立 HTTP 用戶端,請匯入 dart:io

import 'dart:io';

用戶端支援下列 HTTP 作業:GET、POST、PUT 和 DELETE。

final url = Uri.parse('https://httpbin.org/ip');
final httpClient = HttpClient();

Future<void> getIPAddress() async {
  final request = await httpClient.getUrl(url);
  final response = await request.close();
  final responseBody = await response.transform(utf8.decoder).join();
  final ip = jsonDecode(responseBody)['origin'] as String;
  setState(() {
    _ipAddress = ip;
  });
}
API calls on Android
Android
API calls on iOS
iOS

表單輸入

文字欄位允許使用者在您的應用程式中輸入文字,因此可用於建立表單、訊息應用程式、搜尋體驗等。Flutter 提供兩個核心文字欄位小工具:TextFieldTextFormField

如何使用文字欄位小工具?

在 React Native 中,若要輸入文字,請使用 TextInput 元件顯示文字輸入方塊,然後使用回呼函式將值儲存在變數中。

// React Native
const [password, setPassword] = useState('')
...
<TextInput
  placeholder="Enter your Password"
  onChangeText={password => setPassword(password)}
/>
<Button title="Submit" onPress={this.validate} />

在 Flutter 中,請使用 TextEditingController 類別管理 TextField 小工具。每當修改文字欄位時,控制器會通知其偵聽器。

偵聽器會讀取文字和選取內容屬性,以了解使用者在欄位中輸入的內容。您可以透過控制器的 text 屬性存取 TextField 中的文字。

final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
  return Column(children: [
    TextField(
      controller: _controller,
      decoration: const InputDecoration(
        hintText: 'Type something',
        labelText: 'Text Field',
      ),
    ),
    ElevatedButton(
      child: const Text('Submit'),
      onPressed: () {
        showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                title: const Text('Alert'),
                content: Text('You typed ${_controller.text}'),
              );
            });
      },
    ),
  ]);
}

在此範例中,當使用者按一下提交按鈕時,警示對話框會顯示文字欄位中輸入的目前文字。這會使用 AlertDialog 小工具來顯示警示訊息,而 TextField 的文字會由 TextEditingControllertext 屬性存取。

如何使用表單小工具?

在 Flutter 中,請使用 Form 小工具,其中 TextFormField 小工具會與提交按鈕一起傳遞為子項。 TextFormField 小工具有一個名為 onSaved 的參數,它會接收一個回呼,並在儲存表單時執行。 FormState 物件用於儲存、重設或驗證每個 FormField,而 FormField 是此 Form 的後代。若要取得 FormState,您可以將 Form.of() 與其祖先為 Form 的內容一起使用,或將 GlobalKey 傳遞至 Form 建構函式,並呼叫 GlobalKey.currentState()

@override
Widget build(BuildContext context) {
  return Form(
    key: formKey,
    child: Column(
      children: <Widget>[
        TextFormField(
          validator: (value) {
            if (value != null && value.contains('@')) {
              return null;
            }
            return 'Not a valid email.';
          },
          onSaved: (val) {
            _email = val;
          },
          decoration: const InputDecoration(
            hintText: 'Enter your email',
            labelText: 'Email',
          ),
        ),
        ElevatedButton(
          onPressed: _submit,
          child: const Text('Login'),
        ),
      ],
    ),
  );
}

以下範例顯示如何使用 Form.save()formKey (為 GlobalKey) 在提交時儲存表單。

void _submit() {
  final form = formKey.currentState;
  if (form != null && form.validate()) {
    form.save();
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
            title: const Text('Alert'),
            content: Text('Email: $_email, password: $_password'));
      },
    );
  }
}
Input on Android
Android
Input on iOS
iOS

特定平台程式碼

在建置跨平台應用程式時,您會希望在各平台中盡可能重複使用程式碼。不過,可能會出現一些情況,讓程式碼依據作業系統而有所不同。這需要透過宣告特定平台來進行個別實作。

在 React Native 中,會使用以下實作

// React Native
if (Platform.OS === 'ios') {
  return 'iOS';
} else if (Platform.OS === 'android') {
  return 'android';
} else {
  return 'not recognised';
}

在 Flutter 中,使用以下實作

final platform = Theme.of(context).platform;
if (platform == TargetPlatform.iOS) {
  return 'iOS';
}
if (platform == TargetPlatform.android) {
  return 'android';
}
if (platform == TargetPlatform.fuchsia) {
  return 'fuchsia';
}
return 'not recognized ';

偵錯

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

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

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

如果您使用的是 IDE,則可以使用 IDE 的偵錯工具來偵錯您的應用程式。

我如何執行熱重新載入?

Flutter 的 Stateful Hot Reload 功能可協助您快速輕鬆地進行實驗、建置 UI、新增功能和修正錯誤。您不必每次變更時都重新編譯應用程式,而是可以立即熱重新載入您的應用程式。應用程式會更新以反映您的變更,且應用程式的目前狀態會保留。

在 React Native 中,iOS 模擬器的捷徑是 ⌘R,而 Android 模擬器則連續按兩下 R。

在 Flutter 中,如果你正在使用 IntelliJ IDE 或 Android Studio,你可以選擇儲存全部 (⌘s/ctrl-s),或者你可以按一下工具列上的熱重載按鈕。如果你正在使用 flutter run 在命令列執行應用程式,請在終端機視窗中輸入 r。你也可以在終端機視窗中輸入 R 來執行完整重新啟動。

如何存取應用程式內開發人員選單?

在 React Native 中,可以透過搖動裝置來存取開發人員選單:iOS 模擬器為 ⌘D,Android 模擬器為 ⌘M。

在 Flutter 中,如果你正在使用 IDE,你可以使用 IDE 工具。如果你使用 flutter run 來啟動應用程式,你也可以在終端機視窗中輸入 h 來存取選單,或輸入以下捷徑

動作 終端機捷徑 偵錯函數和屬性
應用程式的 Widget 層級 w debugDumpApp()
應用程式的渲染樹 t debugDumpRenderTree()
圖層 L debugDumpLayerTree()
無障礙性 S(遍歷順序)或
U(反向命中測試順序)
debugDumpSemantics()
切換 Widget 檢查器 i WidgetsApp. showWidgetInspectorOverride
切換顯示建構線 p debugPaintSizeEnabled
模擬不同的作業系統 o defaultTargetPlatform
顯示效能覆疊 P WidgetsApp. showPerformanceOverlay
將螢幕截圖儲存到 flutter.png s  
要離開 q  

動畫

設計良好的動畫能讓 UI 感覺直覺,有助於提升應用程式的視覺與感受,並改善使用者體驗。Flutter 的動畫支援功能讓實作簡單與複雜的動畫變得容易。Flutter SDK 內含許多 Material Design 小工具,其中包含標準動作效果,而且你可以輕鬆自訂這些效果,以個人化你的應用程式。

在 React Native 中,使用動畫 API 來建立動畫。

在 Flutter 中,使用 Animation 類別和 AnimationController 類別。 Animation 是抽象類別,它了解其目前值和狀態(已完成或已棄用)。 AnimationController 類別讓你播放動畫(順向或逆向),或停止動畫,並將動畫設定為特定值,以自訂動作。

如何新增一個簡單的淡入動畫?

在以下的 React Native 範例中,使用動畫 API 建立動畫元件 FadeInView。定義初始不透明度狀態、最終狀態,以及轉場發生的期間。動畫元件新增在 Animated 元件內部,不透明度狀態 fadeAnim 對應到我們要製作動畫的 Text 元件的不透明度,然後呼叫 start() 來開始動畫。

// React Native
const FadeInView = ({ style, children }) => {
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 10000
    }).start();
  }, []);

  return (
    <Animated.View style={{ ...style, opacity: fadeAnim }}>
      {children}
    </Animated.View>
  );
};
    ...
<FadeInView>
  <Text> Fading in </Text>
</FadeInView>
    ...

若要在 Flutter 中建立相同的動畫,請建立一個名為 controllerAnimationController 物件,並指定持續時間。預設情況下,AnimationController 會在指定持續時間內線性產生介於 0.0 至 1.0 之間的值。每當執行您應用程式的裝置準備顯示新畫面時,動畫控制器就會產生一個新值。通常,這個速率約為每秒 60 個值。

在定義 AnimationController 時,您必須傳入一個 vsync 物件。存在 vsync 可防止畫面外動畫消耗不必要的資源。您可以使用您的有狀態物件作為 vsync,方法是將 TickerProviderStateMixin 加入類別定義。一個 AnimationController 需要一個 TickerProvider,其使用建構函上的 vsync 參數進行設定。

一個 Tween 描述起始值與結束值之間的內插,或從輸入範圍到輸出範圍的對應。若要將 Tween 物件與動畫搭配使用,請呼叫 Tween 物件的 animate() 方法,並傳入您要修改的 Animation 物件。

針對此範例,使用了 FadeTransition 小工具,且 opacity 屬性對應到 animation 物件。

若要開始動畫,請使用 controller.forward()。也可以使用控制器執行其他操作,例如 fling()repeat()。在此範例中,FlutterLogo 小工具用於 FadeTransition 小工具內。

import 'package:flutter/material.dart';

void main() {
  runApp(const Center(child: LogoFade()));
}

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

  @override
  State<LogoFade> createState() => _LogoFadeState();
}

class _LogoFadeState extends State<LogoFade>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 3000),
      vsync: this,
    );
    final CurvedAnimation curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
    animation = Tween(begin: 0.0, end: 1.0).animate(curve);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: animation,
      child: const SizedBox(
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}
Flutter fade on Android
Android
Flutter fade on iOS
iOS

如何將滑動動畫新增至卡片?

在 React Native 中,PanResponder 或協力廠商程式庫可用於滑動動畫。

在 Flutter 中,若要新增滑動動畫,請使用 Dismissible 小工具,並巢狀放置子小工具。

return Dismissible(
  key: Key(widget.key.toString()),
  onDismissed: (dismissDirection) {
    cards.removeLast();
  },
  child: Container(
      //...
      ),
);
Card swipe on Android
Android
Card swipe on iOS
iOS

React Native 和 Flutter 小工具等效元件

下表列出常用 React Native 元件,並對應至 Flutter 小工具和常見小工具屬性。

React Native 元件 Flutter 小工具 說明
按鈕 ElevatedButton 基本凸起按鈕。
  onPressed [必填] 按鈕被輕觸或以其他方式啟動時的回呼。
  Child 按鈕標籤。
     
按鈕 TextButton 基本平面按鈕。
  onPressed [必填] 按鈕被輕觸或以其他方式啟動時的回呼。
  Child 按鈕標籤。
     
ScrollView ListView 直線排列的可捲動小工具清單。
  children ( <Widget> [ ]) 要顯示的子小工具清單。
  controller [ ScrollController ] 可用於控制可捲動小工具的物件。
  itemExtent [ double ] 如果非空,強制子項在捲動方向具有指定的範圍。
  scroll Direction [ Axis ] 捲動檢視捲動的軸線。
     
FlatList ListView.builder 依需求建立的線性陣列小工具的建構函式。
  itemBuilder [required] [IndexedWidgetBuilder] 協助依需求建立子項。此回呼只會呼叫大於或等於零且小於 itemCount 的索引。
  itemCount [ int ] 改善 ListView 估計最大捲動範圍的能力。
     
Image Image 顯示影像的小工具。
  image [required] 要顯示的影像。
  Image. asset 提供多個建構函式,以供指定影像的各種方式。
  width, height, color, alignment 影像的樣式和配置。
  fit 將影像刻在配置期間分配的空間中。
     
Modal ModalRoute 封鎖與先前路由互動的路由。
  animation 驅動路由轉換和先前路由前向轉換的動畫。
     
ActivityIndicator CircularProgressIndicator 沿著圓形顯示進度的的小工具。
  strokeWidth 用於繪製圓形的線條寬度。
  backgroundColor 進度指標的背景顏色。預設為目前主題的 ThemeData.backgroundColor
     
ActivityIndicator LinearProgressIndicator 一個顯示進度條的元件。
  value 此進度指標的值。
     
RefreshControl RefreshIndicator 一個支援 Material「滑動更新」慣例的元件。
  color 進度指標的前景顏色。
  onRefresh 當使用者將更新指標拖曳到足夠遠以表示他們希望應用程式更新時,會呼叫此函式。
     
View Container 一個包含子元件的元件。
     
View Column 一個以垂直陣列顯示其子元件的元件。
     
View Row 一個以水平陣列顯示其子元件的元件。
     
View Center 一個將其子元件置中於其本身內的元件。
     
View Padding 一個透過指定內距將其子元件縮進的元件。
  padding [必填] [ EdgeInsets ] 縮進子元件的空間量。
     
TouchableOpacity GestureDetector 一個偵測手勢的元件。
  onTap 當點擊發生時呼叫回呼。
  onDoubleTap 當點擊在短時間內於同個位置發生兩次時呼叫回呼。
     
TextInput TextInput 系統文字輸入控制項的介面。
  controller [ TextEditingController ] 用於存取和修改文字。
     
文字 文字 顯示具有單一樣式的文字字串的 Text 元件。
  data [ String ] 要顯示的文字。
  textDirection [ TextAlign ] 文字流動的方向。
     
開關 開關 一種 Material Design 開關。
  value [必填] [ boolean ] 這個開關是開啟或關閉。
  onChanged [必填] [ callback ] 當使用者切換開關時呼叫。
     
滑桿 滑桿 用於從一系列值中選取。
  value [必填] [ double ] 滑桿的目前值。
  onChanged [必填] 當使用者為滑桿選取新值時呼叫。