跳至主要內容
目錄

給 React Native 開發人員的 Flutter

目錄

這份文件適用於希望運用現有 RN 知識來使用 Flutter 建置行動應用程式的 React Native (RN) 開發人員。如果您了解 RN 框架的基本原理,則可以使用這份文件作為開始學習 Flutter 開發的方式。

這份文件可以當作食譜使用,您可以跳過章節並尋找與您的需求最相關的問題。

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

#

與 React Native 一樣,Flutter 使用反應式視圖。但是,雖然 RN 會轉譯為原生小工具,但 Flutter 會完全編譯為原生程式碼。Flutter 控制螢幕上的每個像素,這避免了因需要 JavaScript 橋接器而導致的效能問題。

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

  • 為建置網頁、伺服器和行動應用程式提供開放原始碼、可擴充的程式設計語言。
  • 提供一種物件導向、單一繼承的語言,該語言使用 C 樣式語法,並預先編譯 (AOT) 為原生程式碼。
  • 可選擇轉譯為 JavaScript。
  • 支援介面和抽象類別。

以下描述了 JavaScript 和 Dart 之間的一些差異範例。

進入點

#

JavaScript 沒有預先定義的進入函式—您可以定義進入點。

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

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

dart
/// Dart
void main() {}

DartPad 中試試看。

列印至主控台

#

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

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

DartPad 中試試看。

變數

#

Dart 是類型安全的—它結合使用靜態類型檢查和執行階段檢查,以確保變數的值始終與變數的靜態類型相符。雖然類型是強制性的,但有些類型註釋是選擇性的,因為 Dart 會執行類型推斷。

建立和指派變數

#

在 JavaScript 中,無法為變數輸入類型。

Dart 中,變數必須明確輸入類型,或者類型系統必須自動推斷正確的類型。

js
// JavaScript
let name = 'JavaScript';
dart
/// 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

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

DartPad 中試試看。

如需詳細資訊,請參閱關於變數的文件。

檢查 null 或零

#

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

js
// 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
/// Dart
var myNull;
var zero = 0;
if (zero == 0) {
  print('use "== 0" to check zero');
}

DartPad 中試試看。

函式

#

Dart 和 JavaScript 函式大致相似。主要差異在於宣告。

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

DartPad 中試試看。

如需詳細資訊,請參閱關於函式的文件。

非同步程式設計

#

Futures

#

與 JavaScript 一樣,Dart 支援單執行緒執行。在 JavaScript 中,Promise 物件表示非同步操作的最終完成 (或失敗) 及其產生的值。

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

js
// 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
// 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

js
// 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
// 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 中,您需要匯入每個需要的元件。

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

在 Flutter 中,若要使用 Material Design 程式庫中的小工具,請匯入 material.dart 套件。若要使用 iOS 樣式小工具,請匯入 Cupertino 程式庫。若要使用更基本的小工具集,請匯入 Widgets 程式庫。或者,您可以編寫自己的小工具程式庫並匯入它。

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

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

如需詳細資訊,請參閱Flutter 小工具目錄

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

#

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

js
// 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 小工具。

dart
// 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 根小工具內。

dart
// 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 會根據接收的資料或使用者輸入動態變更狀態。

無狀態和有狀態小工具之間的重要差異在於,StatefulWidget 具有 State 物件,可儲存狀態資料並將其延續到樹狀結構重建中,因此不會遺失。

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

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

#

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

js
// 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 函式所示。

dart
/// 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
// 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 檔案來識別應用程式所需的資產。

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

assets 子章節指定應該包含在應用程式中的檔案。每個資源都以相對於 pubspec.yaml 檔案的明確路徑來識別,資源檔案就位於該路徑中。宣告資源的順序並不重要。實際使用的目錄(在此案例中為 assets)並不重要。然而,雖然資源可以放置在任何應用程式目錄中,但最佳實務是將它們放置在 assets 目錄中。

在建置期間,Flutter 會將資源放置到一個稱為資源包的特殊封存檔中,應用程式會在執行階段從該封存檔讀取資源。當在 pubspec.yaml 的資源部分中指定資源路徑時,建置過程會尋找相鄰子目錄中任何具有相同名稱的檔案。這些檔案也會與指定的資源一起包含在資源包中。當為您的應用程式選擇適當解析度的圖片時,Flutter 會使用資源變體。

在 React Native 中,您會將圖片檔案放置在原始碼目錄中並參考它,來新增靜態圖片。

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

在 Flutter 中,在 Widget 的 build 方法中使用 Image.asset 建構函式,將靜態圖片新增至您的應用程式。

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

如需更多資訊,請參閱在 Flutter 中新增資源和圖片

如何透過網路載入影像?

#

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

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

dart
Image.network('https://flutter-docs.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. 如下所示將套件匯入您的應用程式程式碼中
dart
import 'package:flutter/material.dart';

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

您可以在 pub.devFlutter 套件區段中找到許多由 Flutter 開發人員共享的套件。

Flutter 小工具 (Widget)

#

在 Flutter 中,您可以使用 Widget 建置您的 UI,這些 Widget 描述了在目前配置和狀態下視圖應有的外觀。

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

Center Widget 是另一個如何控制配置的範例。若要置中 Widget,請將其包裝在 Center Widget 中,然後使用配置 Widget 進行對齊、行、欄和網格。這些配置 Widget 本身沒有視覺表示。相反地,它們的唯一目的是控制另一個 Widget 配置的某些方面。若要了解 Widget 以某種方式呈現的原因,檢查相鄰的 Widget 通常會很有幫助。

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

如需有關 Widgets 套件中核心 Widget 的更多資訊,請參閱Flutter 基本 WidgetFlutter Widget 目錄Flutter Widget 索引

視圖 (Views)

#

View 容器的等效項目是什麼?

#

在 React Native 中,View 是一個容器,支援使用 Flexbox 進行配置、樣式、觸控處理和輔助功能控制。

在 Flutter 中,您可以使用 Widgets 程式庫中的核心配置 Widget,例如 ContainerColumnRowCenter。如需更多資訊,請參閱配置 Widget目錄。

FlatListSectionList 的等效項目是什麼?

#

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

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

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

ListView 是 Flutter 最常用的捲動 Widget。預設的建構函式會採用明確的子項清單。ListView 最適合少量 Widget。對於大型或無限清單,請使用 ListView.builder,它會按需建置其子項,並且只建置可見的子項。

dart
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 範例。

如何使用 Canvas 繪圖或著色?

#

在 React Native 中,沒有 canvas 元件,因此會使用像 react-native-canvas 這樣的第三方程式庫。

js
// 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 類別來繪製到 canvas。

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

dart
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

版面配置 (Layouts)

#

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

#

在 React Native 中,大部分配置可以使用傳遞給特定元件的屬性來完成。例如,您可以使用 View 元件上的 style 屬性來指定 flexbox 屬性。若要將您的元件排列成欄,您會指定如下屬性:flexDirection: 'column'

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

在 Flutter 中,配置主要由專門設計來提供配置的 Widget,以及控制 Widget 及其樣式屬性來定義。

例如,ColumnRow Widget 會採用子項陣列,並分別以垂直和水平方式對齊它們。Container Widget 會採用配置和樣式屬性的組合,而 Center Widget 會將其子項 Widget 置中。

dart
@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 在其核心 Widget 程式庫中提供了各種配置 Widget。例如,PaddingAlignStack

如需完整清單,請參閱配置 Widget

Layout on Android
Android
Layout on iOS
iOS

如何將小工具分層?

#

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

Flutter 使用 Stack Widget 以圖層方式排列子項 Widget。這些 Widget 可以完全或部分重疊基準 Widget。

Stack Widget 相對於其方塊的邊緣定位其子項。如果您只想重疊多個子項 Widget,則此類別非常有用。

dart
@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 將 Container(在其半透明黑色背景上顯示其 Text)覆蓋在 CircleAvatar 的頂部。Stack 使用對齊屬性和 Alignment 座標來偏移文字。

Stack on Android
Android
Stack on iOS
iOS

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

樣式設定 (Styling)

#

如何設定元件樣式?

#

在 React Native 中,會使用內嵌樣式和 stylesheets.create 來設定元件樣式。

js
// 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 Widget 可以採用 TextStyle 類別作為其 style 屬性。如果您想在多個位置使用相同的文字樣式,您可以建立 TextStyle 類別,並將其用於多個 Text Widget。

dart
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 圖示色彩

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

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

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

Flutter 的 Cupertino (iOS 風格) 套件為目前的 iOS 設計語言提供高保真 Widget。若要使用 CupertinoIcons 字型,請在您專案的 pubspec.yaml 檔案中新增 cupertino_icons 的相依性。

yaml
name: my_awesome_application
dependencies:
  cupertino_icons: ^1.0.8

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

以下範例會將色彩配置從 seed 設定為 deepPurple,並將文字選取範圍設定為 red

dart
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 Widget 的 theme 屬性,為幾乎所有內容建立統一的樣式。

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

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

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

狀態管理

#

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

如需在 Flutter 中管理狀態方法的更多資訊,請參閱狀態管理

StatelessWidget

#

在 Flutter 中,StatelessWidget 是一個不需要狀態變更的 Widget — 它沒有需要管理的內部狀態。

當您描述的使用者介面部分,不依賴於物件本身的組態資訊以及 Widget 膨脹所在的 BuildContext 時,無狀態 Widget 非常有用。

AboutDialogCircleAvatarText 都是繼承 StatelessWidget 的無狀態 Widget 範例。

dart
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 — 它包含不可變的資料。

無狀態 Widget 的 build 方法通常只會在三種情況下被呼叫

  • 當 Widget 被插入到樹狀結構中時
  • 當 Widget 的父級變更其組態時
  • 當它所依賴的 InheritedWidget 發生變更時

StatefulWidget

#

StatefulWidget 是一個會變更狀態的 Widget。使用 setState 方法來管理 StatefulWidget 的狀態變更。呼叫 setState() 會告知 Flutter 框架狀態中發生了一些變更,這會導致應用程式重新執行 build() 方法,以便應用程式可以反映變更。

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

CheckboxRadioSliderInkWellFormTextField 都是繼承 StatefulWidget 的有狀態 Widget 範例。

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

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

  final String title;

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

以下狀態類別 _MyStatefulWidgetState 實作了 Widget 的 build() 方法。當狀態變更時,例如,當使用者切換按鈕時,會使用新的切換值呼叫 setState()。這會導致框架在 UI 中重建此 Widget。

dart
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 的最佳實務做法是什麼?

#

以下是在設計 Widget 時要考慮的一些事項。

  1. 判斷 Widget 應該是 StatefulWidget 還是 StatelessWidget

在 Flutter 中,Widget 要么是有狀態的,要么是無狀態的 — 這取決於它們是否依賴狀態變更。

  • 如果 Widget 會變更 — 使用者與其互動或資料饋送中斷 UI,則它是有狀態的
  • 如果 Widget 是最終的或不可變的,則它是無狀態的
  1. 判斷哪個物件管理 Widget 的狀態 (針對 StatefulWidget)。

在 Flutter 中,有三種主要的方式來管理狀態

  • Widget 管理自己的狀態
  • 父級 Widget 管理 Widget 的狀態
  • 混合搭配方法

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

  • 如果相關的狀態是使用者資料,例如核取方塊的勾選或取消勾選模式,或滑桿的位置,則最好由父級 Widget 管理該狀態。
  • 如果相關的狀態是美觀的,例如動畫,則 Widget 本身最好管理該狀態。
  • 如有疑問,請讓父級 Widget 管理子 Widget 的狀態。
  1. 繼承 StatefulWidget 和 State。

MyStatefulWidget 類別管理自己的狀態 — 它繼承了 StatefulWidget,覆寫了 createState() 方法以建立 State 物件,並且框架會呼叫 createState() 來建構 Widget。在此範例中,createState() 會建立 _MyStatefulWidgetState 的實例,該實例會在下一個最佳實務中實作。

dart
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 新增到 Widget 樹狀結構中。

將您的自訂 StatefulWidget 新增到應用程式的 build 方法中的 Widget 樹狀結構中。

dart
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

Props

#

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

js
// 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 的本機變數或函式。

dart
/// 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 函式來儲存和擷取對應用程式是永久且全域的資料。

js
// 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
dart
import 'package:shared_preferences/shared_preferences.dart';

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

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

路由 (Routing)

#

大多數應用程式都包含多個畫面,用於顯示不同類型的資訊。例如,您可能會有一個產品畫面,顯示影像,使用者可以在其中點選產品影像,以在新畫面上取得關於該產品的更多資訊。

在 Android 中,新畫面是新的 Activity。在 iOS 中,新畫面是新的 ViewController。在 Flutter 中,畫面只是 Widget!若要在 Flutter 中瀏覽到新畫面,請使用 Navigator Widget。

如何在畫面之間導覽?

#

在 React Native 中,有三個主要的導覽器:StackNavigator、TabNavigator 和 DrawerNavigator。每個導覽器都提供了一種組態和定義畫面的方法。

js
// 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 中,有兩個主要的 Widget 用於在畫面之間導覽

  • Route 是應用程式畫面或頁面的抽象概念。
  • Navigator 是一個管理路由的 Widget。

Navigator 定義為一個 Widget,它使用堆疊原則管理一組子 Widget。導覽器管理 Route 物件的堆疊,並提供管理堆疊的方法,例如 Navigator.pushNavigator.pop。路由清單可能會在 MaterialApp Widget 中指定,或者可能會動態建構,例如,在英雄動畫中。以下範例在 MaterialApp Widget 中指定了具名路由。

dart
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 (Widget 在 Widget 樹狀結構中的位置的控制代碼)。路由的名稱會傳遞給 pushNamed 函式,以導覽到指定的路由。

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

您也可以使用 Navigator 的 push 方法,將給定的 Route 新增到最緊密封閉給定 BuildContext 的導覽器的歷程記錄中,並轉換到該路由。在以下範例中,MaterialPageRoute Widget 是一個模態路由,它會使用平台自適應轉換來取代整個畫面。它會將 WidgetBuilder 作為必要參數。

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

如何使用分頁導覽和抽屜導覽?

#

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

索引標籤導覽

#

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

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

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

Flutter 為抽屜和索引標籤導覽提供了幾個專門的 Widget

TabController
協調 TabBarTabBarView 之間的索引標籤選擇。
TabBar
顯示索引標籤的水平列。
Tab
建立 Material Design TabBar 索引標籤。
TabBarView
顯示與目前選取的索引標籤對應的 Widget。
dart
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 Widget 會包裝新的 TabBar Widget 並建立兩個索引標籤。TabBarView Widget 會作為 Scaffold Widget 的 body 參數傳遞。與 TabBar Widget 的索引標籤對應的所有畫面都是 TabBarView Widget 的子級,同時也具有相同的 TabController

dart
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

js
// 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 小工具會在點擊時提供導覽。

dart
@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 中有 Drawer 時,Scaffold 小工具還包含一個 AppBar 小工具,該小工具會自動顯示適當的 IconButton 以顯示 DrawerScaffold 會自動處理邊緣滑動手勢以顯示 Drawer

dart
@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 元件將偵聽器新增至元件。

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

對於更複雜的手勢以及將多個觸控組合為單個手勢,則使用 PanResponder

js
// 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 中。

dart
@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 來進行網路連線—您發出提取請求,然後接收回應以取得資料。

js
// 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

dart
import 'dart:io';

該用戶端支援以下 HTTP 操作:GET、POST、PUT 和 DELETE。

dart
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 元件來顯示文字輸入框,然後使用回呼將值儲存在變數中。

js
// 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 中的文字。

dart
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 小工具來顯示警示訊息來實現的,並且可透過 TextEditingControllertext 屬性來存取 TextField 中的文字。

如何使用表單小工具?

#

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

dart
@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)在提交時儲存表單。

dart
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 中,將使用以下實作

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

在 Flutter 中,請使用以下實作

dart
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 的「具有狀態的熱重載」功能可協助您快速且輕鬆地試驗、建置 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 來存取功能表,或輸入以下快速鍵

動作終端機快速鍵偵錯函數和屬性
應用程式的小工具階層wdebugDumpApp()
應用程式的轉譯樹狀結構tdebugDumpRenderTree()
圖層LdebugDumpLayerTree()
協助工具S(遍歷順序)或
U(反向點擊測試順序)
debugDumpSemantics()
若要切換小工具檢查器iWidgetsApp.showWidgetInspectorOverride
若要切換顯示建構線pdebugPaintSizeEnabled
若要模擬不同的作業系統odefaultTargetPlatform
若要顯示效能重疊PWidgetsApp.showPerformanceOverlay
若要將螢幕快照儲存至 flutter.pngs
若要結束q

動畫 (Animation)

#

設計完善的動畫使 UI 感覺直覺,有助於提升精美應用程式的外觀和風格,並改善使用者體驗。 Flutter 的動畫支援讓您輕鬆實作簡單和複雜的動畫。 Flutter SDK 包含許多具有標準運動效果的 Material Design 小工具,您可以輕鬆自訂這些效果,以個人化您的應用程式。

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

在 Flutter 中,請使用 Animation 類別和 AnimationController 類別。 Animation 是一個抽象類別,可以了解其目前的值及其狀態(已完成或已關閉)。 AnimationController 類別可讓您向前或向後播放動畫,或停止動畫並將動畫設定為特定值以自訂動畫。

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

#

在下面的 React Native 範例中,使用 Animated API 建立動畫元件 FadeInView。會定義初始不透明度狀態、最終狀態和轉換發生的持續時間。動畫元件會新增到 Animated 元件內,不透明度狀態 fadeAnim 會對應到我們要動畫化的 Text 元件的不透明度,然後呼叫 start() 以開始動畫。

js
// 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 的存在可防止螢幕外動畫耗用不必要的資源。您可以透過將 TickerProviderStateMixin 新增至類別定義,將您的具狀態物件用作 vsyncAnimationController 需要一個 TickerProvider,它會使用建構函式上的 vsync 引數來設定。

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

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

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

dart
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 小工具並巢狀處理子項小工具。

dart
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 小工具描述
ButtonElevatedButton基本凸起按鈕。
onPressed [必要]點擊或以其他方式啟動按鈕時的回呼。
Child按鈕的標籤。
ButtonTextButton基本平面按鈕。
onPressed [必要]點擊或以其他方式啟動按鈕時的回呼。
Child按鈕的標籤。
ScrollViewListView以線性方式排列的可捲動小工具清單。
children( <Widget> [ ] ) 要顯示的子項小工具清單。
controller[ ScrollController ] 可用於控制可捲動小工具的物件。
itemExtent[ double ] 如果為非 Null,則強制子項在捲動方向上具有給定的範圍。
scroll Direction[ Axis ] 捲動檢視捲動的軸線。
FlatListListView.builder隨需建立的線性小工具陣列的建構函式。
itemBuilder [必要][IndexedWidgetBuilder] 有助於隨需建置子項。此回呼僅在索引大於或等於零且小於 itemCount 時才會呼叫。
itemCount[ int ] 提升 ListView 估計最大捲動範圍的能力。
ImageImage一個顯示圖片的 Widget。
image [必要]要顯示的圖片。
Image.asset針對各種指定圖片的方式,提供了多個建構子。
width、height、color、alignment圖片的樣式和佈局。
fit將圖片嵌入佈局期間分配的空間中。
ModalModalRoute一個會阻擋與先前路由互動的路由。
animation驅動路由轉換以及先前路由向前轉換的動畫。
ActivityIndicatorCircularProgressIndicator一個沿著圓形顯示進度的 Widget。
strokeWidth用於繪製圓形的線條寬度。
backgroundColor進度指示器的背景顏色。預設為目前主題的 ThemeData.backgroundColor
ActivityIndicatorLinearProgressIndicator一個沿著線條顯示進度的 Widget。
value此進度指示器的值。
RefreshControlRefreshIndicator一個支援 Material「滑動以重新整理」慣用法的 Widget。
color進度指示器的前景色。
onRefresh當使用者將重新整理指示器拖曳到足以顯示他們想要應用程式重新整理時呼叫的函式。
ViewContainer一個環繞子 Widget 的 Widget。
ViewColumn一個以垂直陣列顯示其子 Widget 的 Widget。
ViewRow一個以水平陣列顯示其子 Widget 的 Widget。
ViewCenter一個將其子 Widget 置於自身中心點的 Widget。
ViewPadding一個使用給定邊距嵌入其子 Widget 的 Widget。
padding [必要][ EdgeInsets ] 嵌入子 Widget 的空間量。
TouchableOpacityGestureDetector一個偵測手勢的 Widget。
onTap當發生點擊時的回呼。
onDoubleTap當在快速連續的時間內,在相同位置發生兩次點擊時的回呼。
TextInputTextInput系統文字輸入控制項的介面。
controller[ TextEditingController ] 用於存取和修改文字。
文字文字Text Widget,以單一樣式顯示文字字串。
data[ String ] 要顯示的文字。
textDirection[ TextAlign ] 文字流動的方向。
SwitchSwitch一個 Material Design 開關。
value [必要][ boolean ] 此開關是否開啟或關閉。
onChanged [必要][ callback ] 當使用者切換開關開啟或關閉時呼叫。
SliderSlider用於從值的範圍中選擇。
value [必要][ double ] 滑桿的目前值。
onChanged [必要]當使用者為滑桿選取新值時呼叫。