給 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
的相關文件。
async
和 await
async
函式宣告定義了一個非同步執行的函式。
在 JavaScript 中, async
函式回傳一個 Promise
,
await
運算子用於等待 Promise
。
// JavaScript
class Example {
async function _getIPAddress() {
const url = 'https://httpbin.org/ip';
const response = await fetch(url);
const json = await response.json();
const data = json.origin;
return data;
}
}
async function main() {
const example = new Example();
try {
const ip = await example._getIPAddress();
console.log(ip);
} catch (error) {
console.error(error);
}
}
main();
在 Dart 中,async
函式回傳一個 Future
,而函式體會在未來執行,
await
運算子用於等待 Future
。
// Dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class Example {
Future<String> _getIPAddress() async {
final url = Uri.https('httpbin.org', '/ip');
final response = await http.get(url);
final ip = jsonDecode(response.body)['origin'] as String;
return ip;
}
}
/// An async function returns a `Future`.
/// It can also return `void`, unless you use
/// the `avoid_void_async` lint. In that case,
/// return `Future<void>`.
void main() async {
final example = Example();
try {
final ip = await example._getIPAddress();
print(ip);
} catch (error) {
print(error);
}
}
如果想了解更多相關訊息,請參考
async
和 await
的相關文件。
基本知識
如何建立一個 Flutter 應用?
如果要建立 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 庫中的
Center
和 Text
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!” 應用程式的介面。


現在大家已經明白了最基本的 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!」應用更多的功能。


當編寫應用程式碼的時候,你將用到下述兩種 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
類別的範例:


專案結構和資源
該從哪開始寫程式碼呢?
從 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 中,你可以在 Image
的 source
屬性中設定 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 需要按照如下的步驟:
-
執行
flutter pub add
將google_sign_in
新增為依賴:
$ flutter pub add google_sign_in
-
在指令行中輸入
flutter pub get
來安裝程式碼套件。如果使用 IDE,它自己會執行flutter pub get
,或者它會提示你是不是要執行該指令。 -
向下面程式碼一樣在程式中引用 package:
import 'package:flutter/material.dart';
如果想了解更多相關訊息,請參考 在 Flutter 裡使用 Packages 和 Flutter Packages 的開發和送出。
你可以在 pub.dev 的 Flutter packages 找到開發者們分享的 package。
Flutter widgets
在 Flutter 中,你可以基於 widget 打造你自己的 UI,透過 widget 當前的設定和狀態會呈現相應的頁面效果。
Widget 常常透過很多小的、單一功能的 widget 組成,透過這樣的封裝往往能夠實現很棒的效果。比如,Container
widget 包含多種 widget,分別負責佈局、繪圖、位置變化和尺寸變化。準確的說,Container
widget 包括 LimitedBox
、ConstrainedBox
、Align
、
Padding
、DecoratedBox
和 Transform
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,比如 Container
、Column
、Row
和 Center
。如果想了解更多相關訊息,請參考 佈局類 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]);
},
);


如果要了解如何實現無限滑動清單,請參考 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 中,你可以使用 CustomPaint
和 CustomPainter
在畫布上進行繪製。
下面的範例程式碼展示了如何使用 CustomPaint
進行繪圖。它實現了抽象類別 CustomPainter
,然後將它賦值給 CustomPainter
的 painter 屬性。
CustomPainter
子類別必須實現 paint
和 shouldRepaint
方法。
class MyCanvasPainter extends CustomPainter {
const MyCanvasPainter();
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = Colors.amber;
canvas.drawCircle(const Offset(100, 200), 40, paint);
final Paint paintRect = Paint()..color = Colors.lightBlue;
final Rect rect = Rect.fromPoints(
const Offset(150, 300),
const Offset(300, 400),
);
canvas.drawRect(rect, paintRect);
}
@override
bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
}
class MyCanvasWidget extends StatelessWidget {
const MyCanvasWidget({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: CustomPaint(painter: MyCanvasPainter()),
);
}
}


佈局
如何使用 widget 來定義佈局屬性?
在 RN 中,大多數佈局需要透過向指定的元件傳遞屬性引數進行設定。比如,你可以使用 View
的 style
來設定 flexbox 屬性。如果要整理一列的元件,你可以使用如下的屬性設定:flexDirection: 『column』
。
// React Native
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
在 Flutter 中,佈局主要是由專門的 widget 定義的,它們同控制類 widget 和樣式屬性一起發揮功能。
比如,Column
和 Row
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。比如 Padding
、Align
和 Stack
。
要得到完整的 widget 清單,請參考 Layout Widgets。


如何為 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
類別的文件。
風格化
如何設定元件的風格?
在 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,
),
),
],
),
);


Icons
和 Colors
呢?
我如何使用 RN 並不包含預設圖示,所以需要使用第三方庫。
在 Flutter 中,引用 Material 函式庫的時候就同時引入了 Material icons 和 colors。
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。
AboutDialog
、CircleAvatar
和 Text
是
StatelessWidget
的子類別,並且是很典型的無狀態 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 會隨著使用者填寫表單或者移動滑區塊的時候發生改變。亦或者隨著資料來源更新的時候發生改變。
Checkbox
、Radio
、Slider
、InkWell
、
Form
、和 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 的最佳實踐是什麼?
下面有一些設計原則供大家參考。
-
確定一個 widget 應該是
StatefulWidget
還是StatelessWidget
。
在 Flutter 中, widget 要麼是有狀態的,要麼是無狀態的。這取決於 widget 是否依賴狀態的改變。
-
如果一個 widget 發生了改變,而它所處的使用者介面或者資料中斷了 UI,那麼該 widget 就是 有狀態 的。
-
如果一個 widget 是 final 型別或者 immutable 型別的,那麼該 widget 是 無狀態 的。
-
確定哪個物件來控制 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 的狀態。
-
繼承
StatefulWidget
和State
。
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) {
//...
}
}
-
將 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'),
);
}
}


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


本地儲存
如果你不需要在本地儲存太多資料同時也不需要儲存結構化資料,那麼你可以使用 shared_preferences
,透過它來讀寫一些原始資料型別鍵值對,資料型別包括布林、浮點、整數、長精度和字串。
如何儲存在應用程式中全域有效的鍵值對?
在 React Native,可以使用 AsyncStorage
中的
setItem
和 getItem
函式來儲存和讀取應用程式中的全域資料。
// 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 add
將 shared_preferences
新增為依賴:
$ flutter pub add shared_preferences
import 'package:shared_preferences/shared_preferences.dart';
要實現持久資料儲存,使用 SharedPreferences
類提供的 setter 方法即可。
Setter 方法適用於多種原始型別資料,比如 setInt
、setBool
、和 setString
。要讀取資料,使用 SharedPreferences
類別中相應的 getter 方法。每一個 setter 方法都有對應的 getter 方法,比如,getInt
、getBool
和 getString
。
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 實現頁面之間的切換:
Navigator
以堆疊的方式管理子 widget。它的堆疊裡儲存的是 Route
物件,並且提供方法管理整個堆疊,比如 Navigator.push
和 Navigator.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 中,createBottomTabNavigator
和 TabNavigation
用來顯示 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 選項與 TabBar
和 TabBarView
結合起來使用就需要 TabController
。
TabController
的建構式函式中的 length
引數定義了 tab 的總數。當狀態變化時,需要使用 TickerProvider
來觸發通知。
TickerProvider
是 vsync
,當你需要建立新的 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 套件,然後使用 createDrawerNavigator
和 DrawerNavigation
實現。
// React Native
export default (MyApp1 = DrawerNavigator({
Home: {
screen: SimpleApp
},
Screen2: {
screen: drawerScreen
}
}));
在 Flutter 中,我們可以結合 Drawer
和 Scaffold
一起使用來實現 Material Design 風格的 drawer 佈局。如果要在應用程式中新增 Drawer
,可以將它封裝在 Scaffold
widget 中。
Scaffold
widget 提供了一種一致的介面風格,它遵循 Material Design 的設計原則。同時它還支援一些特殊的 Material Design 元件,比如 Drawers
,AppBars
,和 SnackBars
。
Drawer
就是一個 Material Design 窗格,它可以從 Scaffold
邊緣水平滑動顯示應用程式的導向選項。你可以在裡面新增 ElevatedButton
或 Text
。或者新增一個清單的元素作為 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
中有一個Drawer
。
Scaffold
會自動處理邊緣的滑動手勢來顯示 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(),
);
}


手勢檢測和觸控事件處理
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 類。


發起 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 add
將 http
新增為依賴:
$ 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;
});
}


輸入表單
TextField 用於在應用程式中輸入文字,這樣就可以實現建立表單、短消息應用、搜尋框等等功能。Flutter 提供兩個核心文字輸入 widget:
TextField
和 TextFormField
.
如何使用文字輸入 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 時,就會用到 Form
。
TextFormField
內含一個 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'));
},
);
}
}


平台相關程式碼
當建立跨平臺應用程式的時候,你會盡量多地複用程式碼。然而,根據不同的應用場景,程式碼會根據平台的不同有所變化。這就需要提前宣告具體的平台來進行獨立的實現。
在 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
來開啟選單,或者參考下面的快捷鍵說明:
w |
debugDumpApp() | |
t |
debugDumpRenderTree() | |
L |
debugDumpLayerTree() | |
S (遍歷順序) 或者U (反轉點選測試順序) |
debugDumpSemantics() | |
i |
WidgetsApp. showWidgetInspectorOverride | |
p |
debugPaintSizeEnabled | |
o |
defaultTargetPlatform | |
P |
WidgetsApp. showPerformanceOverlay | |
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(),
),
);
}
}


如何為卡片新增滑動動畫呢?
在 RN 中,無論 PanResponder
或者第三方庫都可被用於滑動動畫。
在 Flutter 中,要新增滑動動畫,使用 Dismissible
widget 封裝其它子 widget 即可。
return Dismissible(
key: Key(widget.key.toString()),
onDismissed: (dismissDirection) {
cards.removeLast();
},
child: Container(
//...
),
);


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 |
|||
children | ( <Widget> [ ]) 要顯示的子 widget 清單 | |||
controller | [ ScrollController ] 可用於控制滑動 widget 的物件 |
|||
itemExtent | [ double ] 如果非空,那麼強制所有子 widget 在滑動方向上增加給定的距離 | |||
scroll Direction | [ Axis ] 滑動頁面的滑動軸 |
|||
FlatList |
ListView.builder |
|||
itemBuilder [required] | [IndexedWidgetBuilder ] 根據需要建立子 widget。當元素序號大於等於零並且小於佇列元素總數時,該回調函式會被呼叫。 |
|||
itemCount | [ int ] 最佳化了 ListView 對於最大滑動範圍的預估能力。 | |||
Image |
Image |
|||
image [required] | 要顯示的圖片 | |||
Image. asset | 有多個建構式函式可以用於指定圖片。 | |||
width, height, color, alignment | 圖片的風格和佈局。 | |||
fit | 將圖片內嵌到佈局對應的空間裡。 | |||
Modal |
ModalRoute |
|||
animation | 路徑切換的動畫和之前路徑向前切換的動畫。 | |||
ActivityIndicator |
LinearProgressIndicator |
|||
strokeWidth | 圓形線條的寬度。 | |||
backgroundColor | 指示進度的背景色。預設是當前主題的 ThemeData.backgroundColor 。 |
|||
ActivityIndicator |
LinearProgressIndicator |
|||
value | 進度條的進度值。 | |||
RefreshControl |
RefreshIndicator |
|||
color | 進度指示的前景色。 | |||
onRefresh | 當用戶拖拽重新整理指示器想要重新整理的時候會呼叫該函式。 | |||
View |
Container |
|||
View |
Column |
|||
View |
Row |
|||
View |
Center |
|||
View |
Padding |
將子 widget 按照給定的間隔進行排列的 widget。 | ||
padding [required] | [ EdgeInsets ] 子 widget 間隔。 | |||
TouchableOpacity |
GestureDetector |
|||
onTap | 當點選的時候會呼叫。 | |||
onDoubleTap | 當兩次點選的時候會呼叫。 | |||
TextInput |
TextInput |
|||
controller | [ TextEditingController ] 用於獲取或者修改文字。 |
|||
Text |
Text |
|||
data | [ String ] 要顯示的文字。 | |||
textDirection | [ TextAlign ] 文字的方向。 |
|||
Switch |
Switch |
|||
value [required] | [ boolean ] 開關的開啟或者閉合狀態。 | |||
onChanged [required] | [ callback ] 當用戶點選開關的時候呼叫。 | |||
Slider |
Slider |
|||
value [required] | [ double ] 當前滑動器的值。 | |||
onChanged [required] | 當用戶為滑動器選擇了新的值時會呼叫 |