目錄

給 React Native 開發者的 Flutter 指南

目錄

本文面向希望基於現有的 React Native(下文統稱 RN)的知識結構使用 Flutter 開發行動端應用的開發者。如果你已經對 RN 的框架有所瞭解,那麼你可以透過這個文件入門 Flutter 開發。

本文可以當做查詢手冊使用,裡面涉及到的問題基本上可以滿足需求。

針對 JavaScript (ES6) 開發者的 Dart 介紹

與 RN 一樣,Flutter 使用回應式風格的介面編寫方式。然而,RN 需要被轉譯為本地對應的 widget,而 Flutter 是直接編譯成原生程式碼執行。 Flutter 可以控制螢幕上的每一個畫素,由此可以避免使用 JavaScript Bridge 導致的效能問題。

Dart 學習起來非常簡單,包含如下屬性:

  • 它針對 web 服務和移動應用開發提供了一種開源的,可擴充套件的程式語言。

  • 它提供了一種物件導向的單繼承語言,使用 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

如果想了解更多詳細內容,請檢視這個文件 variables

檢查 null 或者零值

在 JavaScript 中,1 或者任何非空物件在使用 == 比較運算子時都會被隱式轉換為 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

如果想了解更多相關訊息,可以參考 函式 介紹。

非同步程式設計

Futures

Dart 與 JavaScript 類似,同樣是單線程模型。在 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);
  }
}

如果想了解更多相關訊息,請參考 asyncawait 的相關文件

基本知識

如何建立一個 Flutter 應用?

如果要建立 RN 應用,你需要在指令行裡執行 create-react-native-app

$ create-react-native-app <projectname>

要建立 Flutter 應用,完成下面其中一項即可:

  • 使用帶有 Flutter 和 Dart 外掛的 IDE。

  • 在指令行中執行指令 flutter create。不過要提前確認 Flutter SDK 已經在系統環境變數 PATH 中定義。

$ flutter create <projectname>

如果想要瞭解更多內容,詳見 開始使用 Flutter,在該頁面會手把手教你建立一個點選按鈕進行計數的應用。建立一個 Flutter 專案就可以建立 Android 和 iOS 裝置上執行應用所需的所有檔案。

我如何執行應用呢?

你可以在 RN 的專案資料夾中執行 npm run 或者 yarn run 以執行應用。

而想執行 Flutter 應用,你可以透過如下幾個途徑進行操作:

  • 在帶有 Flutter 和 Dart 外掛的 IDE 中使用 “run” 選項。

  • 在專案根目錄執行 flutter run

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

如果想了解更多相關訊息,可以參考 Flutter 的相關文件: 開始使用 Flutter

如何匯入 widget

在 RN 中,你需要匯入每一個所需的元件。

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

在 Flutter 中,如果要使用 Material Design 庫裡的 widget,匯入 material.dart package。如果要使用 iOS 風格的 widget,匯入 Cupertino 函式庫。如果要使用更加基本的 widget,匯入 Widgets 函式庫。或者,你可以實現自己的 widget 函式庫並匯入。

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

無論你匯入哪個函式庫,Dart 僅僅引用你應用中用到的 widget。

如果想了解更多相關訊息,可以參考 核心 Widget 目錄

在 Flutter 裡有沒有類似 React Native 中 “Hello world!” 應用程式?

在 RN 裡,HelloWorldApp 繼承自 React.Component 並且透過回傳 view 物件實現了 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 中,你可以使用核心 widget 庫中的 CenterText widget 建立對應的「Hello world!」應用程式。 Center widget 是 widget 樹中的根節點,而且只有 Text 一個子 widget。

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

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

下面的圖片展示了 Android 和 iOS 中的基本 Flutter “Hello world!” 應用程式的介面。

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

現在大家已經明白了最基本的 Flutter 應用,接下來會告訴大家如何利用 Flutter 豐富的 widget 函式庫來建立主流的華麗的應用程式。

我如何使用 widget 並且把它們封裝起來組成一個 widget 樹?

在 Flutter 中,幾乎任何元素都是 widget。

Widget 是建立應用軟體使用者介面的基本元素。你可以將 widget 按照一定的層次組合,成為 widget 樹。每個 widget 內嵌在父 widget 中,並且繼承了父 widget 的屬性,甚至應用程式本身就是一個 widget。並沒有一個獨立的應用程式物件。反而根 widget 充當了這個角色。

一個 widget 可以定義為:

  • 類似按鈕或者選單的結構化元素

  • 類似字型或者顏色方案的風格化元素

  • 類似填充區或者對齊元素佈局元素

下面的範例展示了使用 Material 庫裡 widget 實現的「Hello world!」應用程式。在這個範例中,該 widget 樹是包含在 MaterialApp root widget 裡的。

// 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 widget 所實現的「Hello world!」應用。你可以獲得比「Hello world!」應用更多的功能。

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

當編寫應用程式碼的時候,你將用到下述兩種 widget: 無狀態 widget 就像它的名字一樣,是一個沒有狀態的 widget。無狀態 widget 一旦建立,就不會改變。而 有狀態 widget 會基於接收到的資料或者使用者輸入的資料動態改變狀態。

無狀態 widget 和有狀態 widget 之間的主要區別,是有狀態 widget 包含一個 State 物件會快取狀態資料,並且 widget 樹的重建也會攜帶該資料,因此狀態不會丟失。

在簡單的或者基本的應用程式中,封裝 widget 非常簡單,但是隨著程式碼量的增加並且應用程式的功能變得更加複雜,你應該將層級複雜的 widget 封裝到函式中或者稍小一些的類。建立獨立的函式和 widget 可以讓你更好地複用應用中元件。

如何建立可複用的元件?

在 RN 中,你可以定義一個類來建立一個可複用的元件然後使用 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 中,定義一個類來建立一個自定義 widget 然後複用這個 widget。你可以定義並且呼叫函式來回傳一個可複用的 widget,正如下面範例中 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

專案結構和資源

該從哪開始寫程式碼呢?

main.dart 檔案開始。這個檔案會在你建立 Flutter 應用時自動生成。

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

在 Flutter 中,入口檔案是 {專案目錄}/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.
┬
└ 專案目錄
  ┬
  ├ android      - 包含 Android 相關檔案。
  ├ build        - 儲存 iOS 和 Android 建立檔案。
  ├ ios          - 包含 iOS 相關檔案。
  ├ lib          - 包含外部可訪問 Dart 源檔案。
    ┬
    └ src        - 包含附加源檔案。
    └ main.dart  - Flutter 程式入口和新應用程式的起點。當你建立 Flutter 工程的時候會自動生成這些檔案。你從這裡開始寫 Dart 程式碼
  ├ test         - 包含自動測試檔案。
  └ pubspec.yaml - 包含 Flutter 應用程式的元資料。這個檔案相當於 RN 裡的 package.json 檔案。

我該把資源檔案放到哪並且如何呼叫呢?

一個 Flutter 資源就是打包到你應用程式裡的一個檔案並且在程式執行的時候可以訪問。 Flutter 應用程式可以包含下述幾種資源型別:

  • 類似 JSON 檔案的靜態資料

  • 設定檔案

  • 圖示和圖片(JPEG、PNG、GIF、WebP、BMP 和 WBMP)

Flutter 使用 pubspec.yaml 檔案來確定應用程式中的資源。該檔案在工程的根目錄。

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

assets 確定了需要包含在應用程式中的檔案。每個資源都會在 pubspec.yaml 中定義所儲存的相對路徑。資源定義的順序沒有特殊要求。實際的資料夾(在這裡指 assets )也沒影響。但是,由於資源可以放置於程式的任何目錄,所以放在 assets 資料夾是比較好的。

在建立期間,Flutter 會將資源放到一個稱為 asset bundle 的歸檔檔案中,應用程式可以在執行時訪問該檔案。當一個資源在 pubspec.yaml 中被宣告時,建立程序會查詢和這個檔案相關的子資料夾路徑,這些檔案也會被包含在 asset bundle 中。當你為應用程式選擇和螢幕顯示解析度相關的圖片時, Flutter 會使用資源變體。

在 RN 中,你可以在原始碼資料夾中透過新增檔案來增加一個靜態圖片並且在程式碼中引用它。

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

在 Flutter 中,如果要展示靜態資源圖片,在 widget 的 build 方法中使用 Image.asset 構造即可。

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

如果想了解更多相關訊息,請參考文件 在 Flutter 中新增資源和圖片

如何在網路中載入圖片?

在 RN 中,你可以在 Imagesource 屬性中設定 uri 和所需的尺寸。

在 Flutter 中,使用 Image.network 建構式函式來實現透過地址載入圖片的操作。

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

我如何安裝依賴套件和套件外掛?

Flutter 支援使用開發者向 Flutter 和 Dart 生態系統貢獻的 package。這樣可以使大量開發者快速建立應用程式而無需重複造車輪。而平台相關的 package 就被稱為外掛。

在 RN 中,你可以在指令行中執行 yarn add {package-name} 或者 npm install --save {package-name} 來安裝程式碼套件。

在 Flutter 中,安裝 package 需要按照如下的步驟:

  1. 執行 flutter pub addgoogle_sign_in 新增為依賴:

$ flutter pub add google_sign_in
  1. 在指令行中輸入 flutter pub get 來安裝程式碼套件。如果使用 IDE,它自己會執行 flutter pub get,或者它會提示你是不是要執行該指令。

  2. 向下面程式碼一樣在程式中引用 package:

import 'package:flutter/material.dart';

如果想了解更多相關訊息,請參考 在 Flutter 裡使用 PackagesFlutter Packages 的開發和送出

你可以在 pub.devFlutter packages 找到開發者們分享的 package。

Flutter widgets

在 Flutter 中,你可以基於 widget 打造你自己的 UI,透過 widget 當前的設定和狀態會呈現相應的頁面效果。

Widget 常常透過很多小的、單一功能的 widget 組成,透過這樣的封裝往往能夠實現很棒的效果。比如,Container widget 包含多種 widget,分別負責佈局、繪圖、位置變化和尺寸變化。準確的說,Container widget 包括 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform widget。與其繼承 Container 來實現自定義效果,不如直接修改這些 widget 來實現效果。

Center widget 是另一個用於控制佈局的範例。如果要居中一個 widget,就把它封裝到 Center widget 中,然後使用佈局 widget 來進行對齊行、列和網格。這些佈局 widget 並不可見。而他們的作用就是控制其它 widget 的佈局。如果想搞清楚為什麼一個 widget 會有這樣的效果,有效的方法是研究它臨近的 widget。

如果想了解更多相關訊息,請參考 Flutter 技術概覽

如果想了解更多關於 Widgets 套件中的核心 widget,請參考 基礎 Flutter Widgets核心 Widget 目錄 或是 Flutter Widget 目錄

檢視

View 等價容器的是什麼?

在 RN 中, View 是支援 Flexbox 佈局、風格化、觸控事件處理和訪問性控制的容器。

在 Flutter 中,你可以使用 Widgets 庫中的核心佈局 widget,比如 ContainerColumnRowCenter。如果想了解更多相關訊息,請參考 佈局類 Widgets 目錄。

FlatList 或者 SectionList 相對應的是什麼?

List 是一個可以捲動的縱向排列的元件清單。

在 RN 中,FlatList 或者 SectionList 用於渲染簡單的或者分組的清單。

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

ListView 是 Flutter 最常用的滑動 widget。預設建構式函式需要一個資料清單的引數。 ListView 非常適合用於少量子 widget 的清單。如果清單的元素比較多,可以使用 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 範例應用。

如何使用 Canvas 繪圖?

在 RN 中,canvas 元件是不可見的,所以需要使用類似 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,然後將它賦值給 CustomPainter 的 painter 屬性。 CustomPainter 子類別必須實現 paintshouldRepaint 方法。

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

佈局

如何使用 widget 來定義佈局屬性?

在 RN 中,大多數佈局需要透過向指定的元件傳遞屬性引數進行設定。比如,你可以使用 Viewstyle 來設定 flexbox 屬性。如果要整理一列的元件,你可以使用如下的屬性設定:flexDirection: 『column』

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

在 Flutter 中,佈局主要是由專門的 widget 定義的,它們同控制類 widget 和樣式屬性一起發揮功能。

比如,ColumnRow widget 接受一個陣列的子元素並且分別按照縱向和橫向進行排列。 Container widget 包含佈局和樣式屬性的組合, Center widget 會將其自 widget 也設定居中。

@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 Widgets

Layout on Android
Android
Layout on iOS
iOS

如何為 widget 分層?

在 RN 中,元件可以透過 absolute 劃分層次。

在 Flutter 中使用 Stack widget 將子 widget 進行分層。該 widget 可以將整體或者部分的子 widget 進行分層。

Stack widget 將子 widget 根據容器的邊界進行佈局。如果你僅僅想把子 widget 重疊擺放的話,這個 widget 非常合適。

@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 類別的文件。

風格化

如何設定元件的風格?

在 RN 中,行內風格化和 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 widget 可以接受 TextStyle 作為它的風格化屬性。如果你想在不同的場合使用相同的文字風格,你可以建立一個 TextStyle 類,並且在多個 Text widget 中使用它。

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 呢?

RN 並不包含預設圖示,所以需要使用第三方庫。

在 Flutter 中,引用 Material 函式庫的時候就同時引入了 Material iconscolors

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-style) package 為 iOS 設計語言提供高解析度的 widget。要使用 CupertinoIcons 字型,在專案的 pubspec.yaml 檔案中新增 cupertino_icons 的依賴即可。

name: my_awesome_application
dependencies:
  cupertino_icons: ^1.0.6

要在全域範圍內自定義元件的顏色和風格,使用 ThemeData 為不同的主題指定預設顏色。在 MaterialApp 的主題屬性中設定 ThemeData 物件。 Colors 類提供 Material Design color palette 中所提供的顏色設定。

下面的範例程式碼將主色調設定為 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(),
  );
}

Theme 可以在不使用 MaterialApp widget 的情況下使用。 Theme 接受一個 ThemeData 引數,並且將 ThemeData 應用於它的全部子 widget。

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

狀態管理

當 widget 被建立或者在 widget 的生命週期中有訊息發生改變時所產生的訊息叫做狀態。要在 Flutter 中管理應用程式的狀態,使用 StatefulWidget 和 State 物件。

欲知更多關於 Flutter 的狀態管理相關的內容,請參訪 狀態管理文件 頁面。

管理 StatelessWidget widget

StatelessWidget 在 Flutter 中是一個不需要狀態改變的 widget,它沒有內部的狀態。

當你展現給使用者的介面並不依賴其它任何設定訊息並且使用 BuildContext 來解析 widget,則需要使用無狀態 widget。

AboutDialogCircleAvatarTextStatelessWidget 的子類別,並且是很典型的無狀態 widget。

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 類別的建構式函式來傳遞 text。並且它被標記為 final。該類別繼承了 StatelessWidget,它包含不可變的資料。

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

  • 當 widget 被插入到 widget 樹中;

  • 當 widget 的父 widget 改變了設定;

  • 當所依賴的 InheritedWidget 發生了改變。

The StatefulWidget

StatefulWidget widget

StatefulWidget 是攜帶狀態變化的 widget。透過呼叫 setState 方法可以管理 StatefulWidget 的狀態。當呼叫 setState() 的時候,程式會通知 Flutter 框架有狀態發生了改變,然後會重新執行 build() 方法來更新應用的狀態。

狀態 是在 widget 被建立期間可以被同步讀取的訊息,並且在 widget 的生命週期中會發生改變。實現該 widget 的時候要注意保證黨狀態發生改變的時候程式能夠獲得相應的提醒。當 widget 能夠動態改變的時候,請使用 StatefulWidget。比如,某個 widget 會隨著使用者填寫表單或者移動滑區塊的時候發生改變。亦或者隨著資料來源更新的時候發生改變。

CheckboxRadioSliderInkWellForm、和 TextField 都是有狀態的 widget,是 StatefulWidget 的子類別。

下面的範例程式碼宣告了一個 StatefulWidget,需要實現 createState() 方法。該方法建立一個物件來管理 widget 的狀態,也就是 _MyStatefulWidgetState

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

  final String title;

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

下面的狀態類,_MyStatefulWidgetState,實現了 build() 方法。當狀態發生改變的時候,比如說使用者點選了開關按鈕,這時 setState 就會被呼叫,並且將新的開關狀態傳進來。這就會使整體框架重構這個 widget。

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. 確定一個 widget 應該是 StatefulWidget 還是 StatelessWidget

在 Flutter 中, widget 要麼是有狀態的,要麼是無狀態的。這取決於 widget 是否依賴狀態的改變。

  • 如果一個 widget 發生了改變,而它所處的使用者介面或者資料中斷了 UI,那麼該 widget 就是 有狀態 的。

  • 如果一個 widget 是 final 型別或者 immutable 型別的,那麼該 widget 是 無狀態 的。

  1. 確定哪個物件來控制 widget 的狀態(針對 StatefulWidget)。

在 Flutter 中,有三種途徑來管理狀態:

  • widget 管理它的自身狀態

  • 由其父 widget 管理 widget 狀態

  • 透過混搭的方式

當決定了使用哪個途徑後,要考慮下述的幾個原則:

  • 如果狀態訊息是使用者資料,比如複選框是被勾選還是未被勾選,或者滑區塊的位置,那麼父 widget 會很好的處理當前 widget 的狀態。

  • If the state in question is user data, for example the checked or unchecked mode of a checkbox, or the position of a slider, then the state is best managed by the parent widget.
  • 如果狀態是和外觀效果相關的,比如動畫,那麼 widget 自己會處理狀態的變化。

  • 如果無法確定,那麼父 widget 會處理子 widget 的狀態。

  1. 繼承 StatefulWidgetState

MyStatefulWidget 類管理它自身的狀態—&mdash 它繼承自 StatefulWidget,重寫了 createState() 方法。該方法建立了 State 物件,同時框架會呼叫 createState() 方法來建立 widget。在這個例子中,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 新增到 widget 樹中

將你自定義的 StatefulWidget 透過應用程式的 build 方法新增到 widget 樹中。

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

在 RN 中,大多數元件都可以在建立的時候透過不同的引數或者屬性來自定義,叫做 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 來實現簡單資料的持續儲存。

執行 flutter pub addshared_preferences 新增為依賴:

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

要實現持久資料儲存,使用 SharedPreferences 類提供的 setter 方法即可。 Setter 方法適用於多種原始型別資料,比如 setIntsetBool、和 setString。要讀取資料,使用 SharedPreferences 類別中相應的 getter 方法。每一個 setter 方法都有對應的 getter 方法,比如,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 中,新的頁面是 Activity。在 iOS 中,新的頁面是 ViewController。在 Flutter 中,頁面也只是 widget,如果在 Flutter 中要切換頁面,使用 Navigator widget 即可。

如何在頁面之間進行切換?

在 RN 中,有三種主要的導向 widget : StackNavigator、TabNavigator 和 DrawerNavigator。每個都提供了設定和定義頁面的方法。

// 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。它的堆疊裡儲存的是 Route 物件,並且提供方法管理整個堆疊,比如 Navigator.pushNavigator.pop。路徑清單需要在 MaterialApp 中指定。或者在頁面切換的時候進行建立,比如 hero 動畫。下面的例子在 MaterialApp widget 中指定了頁面切換路徑。

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 函式來切換至指定的路徑。

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

你可以使用 Navigator 中的 push 方法新增 Route 到 navigator 的歷史佇列中,其中包含 BuildContext 並且可以切換到指定頁面。在下面的例子中,MaterialPageRoute widget 是一個模式化路徑,可以將整個頁面透過平台自適應切換方式進行切換。它需要一個 WidgetBuilder 引數。

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

如何使用 tab 導向和 drawer 導向?

在 Material Design 應用程式中, Flutter 的導向形式主要有兩種:tab 和 drawer。如果沒有足夠的 widget 可以容納 tab,drawer 就是個不錯的選擇。

Tab 導向

在 RN 中,createBottomTabNavigatorTabNavigation 用來顯示 tab 和 tab 導向。

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

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

Flutter 針對 drawer 和 tab 導向提供幾種專用的 widget:

TabController
將 tab 與 TabBar 和 TabBarView 結合起來使用。

TabBar
水平顯示一行 tab。

Tab
建立一個 material design 風格的 TabBar 中的 tab。

TabBarView
顯示目前所選 tab 所對應的 widget。

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

要將 tab 選項與 TabBarTabBarView 結合起來使用就需要 TabControllerTabController 的建構式函式中的 length 引數定義了 tab 的總數。當狀態變化時,需要使用 TickerProvider 來觸發通知。 TickerProvidervsync,當你需要建立新的 TabController 時,將 vsync: this 作為建構式函式的引數即可。

TickerProvider 介面可以用於生成 Ticker 物件。當有物件被觸發通知後會用到 Tickers,不過它通常都是被 AnimationController 間接呼叫。 AnimationController 需要 TickerProvider 來獲得對應的 Ticker。如果你透過 State 建立了一個 AnimationController,那麼你就可以使用 TickerProviderStateMixin 或者 SingleTickerProviderStateMixin 來獲得對應的 TickerProvider

Scaffold 封裝了一個新的 TabBar widget,其中包含兩個 tab。 TabBarView 作為 body 引數傳遞到 Scaffold 中。所有和 TabBar 中的 tab 相關的頁面均是 TabBarView 的子 widget,並且都對應同一個 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()],
        ));
  }
}

Drawer 導向

在 RN 中,匯入所需的 react-navigation 套件,然後使用 createDrawerNavigatorDrawerNavigation 實現。

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

在 Flutter 中,我們可以結合 DrawerScaffold 一起使用來實現 Material Design 風格的 drawer 佈局。如果要在應用程式中新增 Drawer,可以將它封裝在 Scaffold widget 中。 Scaffold widget 提供了一種一致的介面風格,它遵循 Material Design 的設計原則。同時它還支援一些特殊的 Material Design 元件,比如 DrawersAppBars,和 SnackBars

Drawer 就是一個 Material Design 窗格,它可以從 Scaffold 邊緣水平滑動顯示應用程式的導向選項。你可以在裡面新增 ElevatedButtonText 。或者新增一個清單的元素作為 Drawer 的子 widget。在下面的例子中,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。它會自動顯示一個圖示按鈕來表明 Scaffold 中有一個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 中的手勢處理有兩個獨立的層。第一層是指標事件,指標事件定義了指標在螢幕上的位置和動作,比如觸控、滑鼠和觸控筆。第二層指手勢,主要是語義層面的動作,裡面包含一種或者多種指標動作。

如何為 widget 新增點選或者按壓的監聽器?

在 RN 中,使用 PanResponder 或者 Touchable 元件來新增監聽器。

// 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 中,要為 widget 新增點選或者按壓監聽器,使用帶有 onPress: field 的按鈕或者可觸控 widget 即可。或者,用任何 widget 封裝 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 呼叫來獲得資料呢?

RN 提供 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 package。

執行 flutter pub addhttp 新增為依賴:

$ 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

輸入表單

TextField 用於在應用程式中輸入文字,這樣就可以實現建立表單、短消息應用、搜尋框等等功能。Flutter 提供兩個核心文字輸入 widget: TextFieldTextFormField.

如何使用文字輸入 widget ?

在 RN 裡,可以使用 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 widget。當用戶修改文字的時候,controller 會通知監聽器。

監聽器讀取文字和選項屬性來獲知使用者所輸入的內容。你可以透過 TextField 中的 text 屬性獲得使用者輸入的文字資料。

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 widget 顯示提示訊息, TextField 的文字透過 text 屬性來獲得,該屬性屬於 TextEditingController

如何使用 Form widget 呢?

在 Flutter 中,當需要使用帶有送出按鈕和 TextFormField 元件的複合 widget 時,就會用到 FormTextFormField 內含一個 onSaved 引數,它可以設定一個回呼函式,當表單儲存的時候會回呼該函式。 FormState 用於儲存、重置或者驗證 Form 內含的每個 FormField。你可以透過將當前表單的 context 屬性賦值給 Form.of 來獲得 FormState。或者在表單的建構式函式裡使用 GlobalKey,然後呼叫 GlobalKey.currentState 來獲得 FormState

@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

平台相關程式碼

當建立跨平臺應用程式的時候,你會盡量多地複用程式碼。然而,根據不同的應用場景,程式碼會根據平台的不同有所變化。這就需要提前宣告具體的平台來進行獨立的實現。

在 RN 中,下面的實現程式碼會被用到:

// 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 應用。

開發者工具包含了 profiling 建立、檢查堆疊、檢視 widget 樹、診斷訊息記錄、除錯、執行程式碼行觀察、除錯記憶體洩漏和記憶體碎片等。有關更多訊息,請參閱 DevTools 文件。

如果你在用 IDE 進行編輯,你可以使用 IDE 的除錯器除錯你的應用。

如何進行熱過載?

Flutter 的熱過載屬性可以幫助你快速便捷地實驗、建立 UI 和各種屬性以及修復 bug。每次修改程式碼以後,你只需直接熱過載你的應用程式即可,而無需重新進行編譯。應用程式會根據你的修改進行相應的更新,而程式原有的狀態則會被保留。

在 RN 中,iOS 模擬器對應的快捷鍵是 ⌘R,對應 Android 模擬器的快捷鍵是點選兩次 R。

在 Flutter 中,如果你使用的是 IntelliJ 或者 Android Studio,可以使用 Save All (⌘s/ctrl-s),或者可以點選工具欄上的 Hot Reload 按鈕。如果你是在指令行裡使用 flutter run 指令執行的程式,在視窗裡輸入 r 即可。也可以輸入 R 進行徹底的重啟。

如何開啟程式裡的開發者選單?

在 RN 中,開發者選單可以透過搖動裝置開啟:對於 iOS 模擬器的快捷鍵是 ⌘D 而 Android 模擬器的快捷鍵是 ⌘M。

在 Flutter 中,如果你使用 IDE,那麼可以直接使用 IDE 工具。如果你是透過指令行執行 flutter run 來啟動應用程式的,你可以在指令行視窗透過輸入 h 來開啟選單,或者參考下面的快捷鍵說明:

功能 指令行快捷鍵 除錯功能和屬性
應用程式的 widget 層級 w debugDumpApp()
渲染程式的 widget 樹 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 widget。其中已經包括了標準的動畫效果,你可以很方便地自定義這些效果。

在 RN 中,動畫 API 用於建立動畫。

在 Flutter 中,使用 Animation 類別和 AnimationController 類實現動畫。 Animation 是抽象類別,內含其當前的值和它的狀態(已完成或者已取消)。 AnimationController 類可以正向或者反向播放動畫或者停止動畫以及為動畫設定特定值來自定義動畫。

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

在下面的 React Native 範例中,有一個動畫元件,也就是 FadeInView,它是使用 Animated API 建立的。定義了初始的不透明狀態,最終狀態和動畫切換之間的時間間隔。在 Animated 中新增了動畫元件,不透明狀態 fadeAnim 對映到我們想要新增動畫效果的文字元件上,然後在開始動畫的時候呼叫 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 中實現相同的動畫效果,建立一個 AnimationController 物件,叫它 controller,並且指定時間間隔。在預設設定下,AnimationController 會在給定時間間隔線性地生成從 0.0 到 1.0 的數值。當你的程式可以顯示新一幀畫面的時候,AnimationController 會生成一個新的值。通常這個頻率為每秒 60 次。

當定義 AnimationController 的時候,你必須傳入一個 vsync 物件。 vsync 會防止螢幕顯示區域之外的動畫消耗不必要的資源。你可以透過新增 TickerProviderStateMixin 到類別定義中來使用有狀態的物件。 AnimationController 需要傳入一個 TickerProvider,它是透過建構式函式裡的 vsync 引數進行設定的。

Tween 定義了起始和結束值之間或者輸入段到輸出段之間的轉場。如果要在動畫中使用 Tween 物件,呼叫 Tween 物件的 animate 方法,然後把它賦給你要修改的 Animation 物件。

在這個例子中,用到了 FadeTransition widget,它的 opacity 屬性對映到了 animation 物件上。

要開始動畫,使用 controller.forward()。其它的操作也可以使用控制器裡的方法,比如 fling() 或者 repeat()。這個例子裡,FlutterLogo widget 被用於 FadeTransition widget 中。

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

如何為卡片新增滑動動畫呢?

在 RN 中,無論 PanResponder 或者第三方庫都可被用於滑動動畫。

在 Flutter 中,要新增滑動動畫,使用 Dismissible widget 封裝其它子 widget 即可。

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 widget 對等的元件

下面的表格列舉了通用的 React Native 元件與對應的 Flutter widget 和通用的 widget 屬性。

React Native 元件 Flutter Widget 描述    
Button ElevatedButton 基礎的凸起按鈕    
  onPressed [required] 該回調函式在當按鈕被點選的時候被觸發。    
  Child 按鈕的標籤    
 Button  TextButton 基礎的扁平化按鈕.    
  onPressed [required] 當按鈕被點選的時候觸發該回調函式。    
  Child 按鈕的標籤    
 ScrollView  ListView 一個可滑動的縱向排列的 widget 清單。    
  children ( <Widget> [ ]) 要顯示的子 widget 清單    
  controller [ ScrollController ] 可用於控制滑動 widget 的物件    
  itemExtent [ double ] 如果非空,那麼強制所有子 widget 在滑動方向上增加給定的距離    
  scroll Direction [ Axis ] 滑動頁面的滑動軸    
 FlatList  ListView.builder 根據需要建立的一組 widget 的建構式函式。    
  itemBuilder [required] [IndexedWidgetBuilder] 根據需要建立子 widget。當元素序號大於等於零並且小於佇列元素總數時,該回調函式會被呼叫。    
  itemCount [ int ] 最佳化了 ListView 對於最大滑動範圍的預估能力。    
 Image  Image 顯示圖片的 widget。    
  image [required] 要顯示的圖片    
  Image. asset 有多個建構式函式可以用於指定圖片。    
  width, height, color, alignment 圖片的風格和佈局。    
  fit 將圖片內嵌到佈局對應的空間裡。    
 Modal  ModalRoute 避免和之前路徑交叉的路徑。    
  animation 路徑切換的動畫和之前路徑向前切換的動畫。    
 ActivityIndicator  LinearProgressIndicator 一個進度條 widget。    
  strokeWidth 圓形線條的寬度。    
  backgroundColor 指示進度的背景色。預設是當前主題的 ThemeData.backgroundColor    
 ActivityIndicator  LinearProgressIndicator 一個水平條形的進度條。    
  value 進度條的進度值。    
 RefreshControl  RefreshIndicator 支援 Material 中滑動重新整理的 widget    
  color 進度指示的前景色。    
  onRefresh 當用戶拖拽重新整理指示器想要重新整理的時候會呼叫該函式。    
 View  Container 封裝子 widget 的 widget。    
 View  Column 將子 widget 縱向排列的 widget。    
 View  Row 將子 widget 橫向排列的 widget。    
 View  Center 將子 widget 放置於中央的 widget。    
View Padding 將子 widget 按照給定的間隔進行排列的 widget。    
  padding [required] [ EdgeInsets ] 子 widget 間隔。    
 TouchableOpacity  GestureDetector 檢測手勢的 widget。    
  onTap 當點選的時候會呼叫。    
  onDoubleTap 當兩次點選的時候會呼叫。    
 TextInput  TextInput 呼叫系統文字輸入的介面。    
  controller [ TextEditingController ] 用於獲取或者修改文字。    
 Text  Text 以單一的樣式顯示文字的文字 widget。    
  data [ String ] 要顯示的文字。    
  textDirection [ TextAlign ] 文字的方向。    
 Switch  Switch Material Design 樣式的開關。    
  value [required] [ boolean ] 開關的開啟或者閉合狀態。    
  onChanged [required] [ callback ] 當用戶點選開關的時候呼叫。    
 Slider  Slider 選擇一個範圍的值。    
  value [required] [ double ] 當前滑動器的值。    
  onChanged [required] 當用戶為滑動器選擇了新的值時會呼叫