Contents

給 React Native 開發者的 Flutter 指南

Contents

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

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

針對 JavaScript 開發者的 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
var 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
var name; // == undefined
// Dart
var name; // == null; raises a linter warning
int? x; // == null

可以在這裡檢視效果 DartPad

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

檢查 null 或者零值

在 JavaScript 中,1 或者任何非空物件在使用 == 比較運運算元時都會被隱含轉換為 true

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

在 Dart 中,只有布林型別值 true 才是 true。

/// Dart
var myNull;
if (myNull == null) {
  print('use "== null" to check null');
}
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) {
      String ip = jsonDecode(response.body)['origin'];
      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);
    String ip = jsonDecode(response.body)['origin'];
    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';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello world!</Text>
      </View>
    );
  }
}

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

在 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
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title="Press"
          onPress={() => this.props.onPress(this.props.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')} />

在 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. pubspec.yaml 的 dependencies 區域新增套件名稱和版本。下面的例子向大家展示瞭如何將 google_sign_in 的 Dart package 新增到 pubspec.yaml 中。一定要檢查一下 YAML 檔案中的空格,因為 空格很重要!

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

  2. 向下面程式碼一樣在程式中參考 package:

import 'package:flutter/material.dart';

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

你可以在 Flutter packagespub.dev 找到開發者們分享的 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
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();
};

render() {
  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.0, 200.0), 40.0, paint);
    final Paint paintRect = Paint()..color = Colors.lightBlue;
    final Rect rect = Rect.fromPoints(
      const Offset(150.0, 300.0),
      const Offset(300.0, 400.0),
    );
    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.0,
          height: 100.0,
        ),
        Container(
          color: Colors.blue,
          width: 100.0,
          height: 100.0,
        ),
        Container(
          color: Colors.green,
          width: 100.0,
          height: 100.0,
        ),
      ],
    ),
  );

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.0,
  fontWeight: FontWeight.w600,
);

return Center(
  child: Column(
    children: const <Widget>[
      Text('Sample text', style: textStyle),
      Padding(
        padding: EdgeInsets.all(20.0),
        child: Icon(
          Icons.lightbulb_outline,
          size: 48.0,
          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: ^0.1.0

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

下面的範例程式碼將主色調設定為 blue 然後文字顏色設定為 red

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
          primarySwatch: Colors.blue,
          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.0),
              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 的狀態。

  • 如果狀態是和外觀效果相關的,比如動畫,那麼 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
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title='Press'
          onPress={() => this.props.onPress(this.props.index)}
        />
      </View>
    );
  }
}
class App extends React.Component {

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

  render() {
    return (
      <View>
        <FlatList
          data={[ ... ]}
          renderItem={({ item }) => (
            <CustomCard onPress={this.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
await AsyncStorage.setItem( 'counterkey', json.stringify(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
  if (value != null) {
    this.setState({ counter: value });
  }
});

在 Flutter 中,使用 shared_preferences 外掛來儲存和存取應用程式內全域有效的鍵值對資料。 shared_preferences 外掛封裝了 iOS 中的 NSUserDefaults 和 Android 中的 SharedPreferences 來實現簡單資料的持續儲存。如果要使用該外掛,可以在 pubspec.yaml 中新增依賴 shared_preferences,然後在 Dart 檔案中參考包即可。

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.13
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 中指定了頁面切換路徑。