給 React Native 開發者的 Flutter 指南
本文面向希望基於現有的 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
的相關文件。
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);
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);
}
}
如果想了解更多相關資訊,請參考
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';
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 庫中的
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
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
類別的範例:


專案結構和資源
該從哪開始寫程式碼呢?
從 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 中,你可以在 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 需要按照如下的步驟:
-
在
pubspec.yaml
的 dependencies 區域新增套件名稱和版本。下面的例子向大家展示瞭如何將google_sign_in
的 Dart package 新增到pubspec.yaml
中。一定要檢查一下 YAML 檔案中的空格,因為 空格很重要!
dependencies:
flutter:
sdk: flutter
google_sign_in: ^3.0.3
-
在命令列中輸入
flutter pub get
來安裝程式碼套件。如果使用 IDE,它自己會執行flutter pub get
,或者它會提示你是不是要執行該命令。 -
向下面程式碼一樣在程式中參考 package:
import 'package:flutter/material.dart';
如果想了解更多相關資訊,請參考 在 Flutter 裡使用 Packages 和 Flutter Packages 的開發和提交。
你可以在 Flutter packages 的 pub.dev 找到開發者們分享的 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
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 中,你可以使用 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.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()),
);
}
}


佈局
如何使用 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.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。比如 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.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,
),
),
],
),
);


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: ^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。
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.0),
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 的狀態。
-
如果狀態是和外觀效果相關的,比如動畫,那麼 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
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');
},
);
}
}


本地儲存
如果你不需要在本地儲存太多資料同時也不需要儲存結構化資料,那麼你可以使用 shared_preferences
,透過它來讀寫一些原始資料型別鍵值對,資料型別包括布林、浮點、整數、長精度和字串。
如何儲存在應用程式中全域有效的鍵值對?
在 React Native,可以使用 AsyncStorage
中的
setItem
和 getItem
函式來儲存和讀取應用程式中的全域資料。
// 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 方法適用於多種原始型別資料,比如 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 中指定了頁面切換路徑。