面向 React Native 開發者的 Flutter 指南
瞭解如何將 React Native 開發經驗應用到 Flutter 應用構建中。
本文件旨在幫助 React Native (RN) 開發者利用現有的 RN 知識構建 Flutter 移動應用。如果您理解 RN 框架的基本原理,可以使用本文件作為學習 Flutter 開發的起點。
本文件可以像菜譜一樣使用,您可以隨意跳轉並查詢與您的需求最相關的問題。
面向 JavaScript (ES6) 開發者的 Dart 簡介
#與 React Native 一樣,Flutter 使用響應式風格的檢視。然而,RN 會轉譯為原生元件,而 Flutter 則直接編譯為機器碼。Flutter 控制螢幕上的每一個畫素,從而避免了因 JavaScript 橋接而帶來的效能問題。
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 中嘗試一下。
有關詳細資訊,請參閱有關 變數 的文件。
檢查 null 或零值
#在 JavaScript 中,使用 == 比較運算子時,值 1 或任何非 null 物件都被視為 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 才被視為真。
/// Dart
var myNull = potentiallyNull();
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(未來物件)
#與 JavaScript 一樣,Dart 支援單執行緒執行。在 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 應用?
#要使用 React Native 建立應用,您可以在命令列中執行 create-react-native-app。
create-react-native-app <projectname>
要在 Flutter 中建立應用,請執行以下任一操作:
- 使用已安裝 Flutter 和 Dart 外掛的 IDE。
- 在命令列中使用
flutter create命令。請確保 Flutter SDK 已新增到您的 PATH 中。
flutter create <projectname>
有關更多資訊,請參閱 入門指南,其中會指導您建立一個點選按鈕的計數器應用。建立一個 Flutter 專案將構建在 Android 和 iOS 裝置上執行示例應用所需的所有檔案。
如何執行應用?
#在 React Native 中,您會在專案目錄下執行 npm run 或 yarn run。
您可以以多種方式執行 Flutter 應用:
- 在裝有 Flutter 和 Dart 外掛的 IDE 中使用“執行”選項。
- 在專案的根目錄中使用
flutter run。
您的應用會在連線的裝置、iOS 模擬器或 Android 模擬器上執行。
有關更多資訊,請參閱 Flutter 入門 文件。
如何匯入元件(Widgets)?
#在 React Native 中,您需要匯入所需的每個元件。
// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
在 Flutter 中,若要使用 Material Design 庫中的元件,請匯入 material.dart 包。若要使用 iOS 風格的元件,請匯入 Cupertino 庫。若要使用更基礎的元件集,請匯入 Widgets 庫。或者,您也可以編寫自己的元件庫並進行匯入。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:my_widgets/my_widgets.dart';
無論您匯入哪個元件包,Dart 只會提取您應用中實際使用的元件。
有關更多資訊,請參閱 Flutter 元件目錄。
Flutter 中是否有類似 React Native 的“Hello world!”應用?
#在 React Native 中,HelloWorldApp 類繼承 React.Component 並實現 render 方法,返回一個檢視元件。
// 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 中,您可以使用核心元件庫中的 Center 和 Text 元件建立一個完全相同的“Hello world!”應用。Center 元件成為元件樹的根,幷包含一個子元件:Text 元件。
// Flutter
import 'package:flutter/material.dart';
void main() {
runApp(
const Center(
child: Text('Hello, world!', textDirection: TextDirection.ltr),
),
);
}
下圖展示了基礎 Flutter "Hello world!" 應用在 Android 和 iOS 上的 UI。
現在您已經瞭解了最基礎的 Flutter 應用,下一節將展示如何利用 Flutter 豐富的元件庫來建立現代、精緻的應用。
如何使用元件並巢狀它們來構建元件樹?
#在 Flutter 中,幾乎一切皆元件。
元件(Widget)是構建應用介面的基礎構建塊。您可以將元件組合成層次結構,稱為“元件樹”。每個元件都巢狀在父元件內,並從父元件繼承屬性。甚至應用物件本身也是一個元件,沒有獨立的“應用”物件。相反,根元件承擔了這一角色。
元件可以定義:
- 結構元素——如按鈕或選單
- 樣式元素——如字型或配色方案
- 佈局細節——如內邊距或對齊方式
以下示例展示了使用 Material 庫中的元件實現的“Hello world!”應用。在此示例中,元件樹巢狀在 MaterialApp 根元件中。
// Flutter
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(title: const Text('Welcome to Flutter')),
body: const Center(child: Text('Hello world')),
),
);
}
}
下圖展示了使用 Material Design 元件構建的“Hello world!”應用。與基礎版相比,您無需額外配置即可獲得更豐富的功能。
編寫應用時,您會使用兩種型別的元件:StatelessWidget 或 StatefulWidget。StatelessWidget 字面意思就是無狀態元件,一旦建立,其外觀便不會改變。StatefulWidget 則根據接收到的資料或使用者輸入動態改變狀態。
無狀態元件和有狀態元件的重要區別在於:StatefulWidget 擁有一個 State 物件,該物件儲存狀態資料,並在元件樹重建時保留這些資料,從而不會丟失狀態。
在簡單應用中,巢狀元件很容易,但隨著程式碼庫增大,應用變得複雜,您應該將深度巢狀的元件拆分為返回元件的函式或更小的類。建立獨立的函式和元件可以方便您在應用內複用元件。
如何建立可複用元件?
#在 React Native 中,您可以定義一個函式(或類)來建立可複用元件,然後使用 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 中,定義一個類來建立自定義元件,然後複用該元件。您也可以定義並呼叫一個返回元件的函式,如下例中的 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 類的示例。
專案結構和資源
#從哪裡開始編寫程式碼?
#從 lib/main.dart 檔案開始,這是建立 Flutter 應用時自動生成的。
// Dart
void main() {
print('Hello, this is the main function.');
}
在 Flutter 中,入口檔案是 {project_name}/lib/main.dart,執行從 main 函式開始。
Flutter 應用的檔案結構是怎樣的?
#建立新 Flutter 專案時,會生成以下目錄結構。您可以稍後進行自定義,但這是您開始的地方。
-
<project_name>/
- android/// 包含 Android 特定檔案。
- build/// 儲存 iOS 和 Android 構建檔案。
- ios/// 包含 iOS 特定檔案。
lib/// 包含外部可訪問的 Dart 原始碼檔案。
- src/// 包含其他原始碼檔案。
- main.dart// Flutter 入口點,應用啟動的開始。專案建立時自動生成,您從這裡開始編寫 Dart 程式碼。
- test/// 包含自動化測試檔案。
- pubspec.yaml// 包含 Flutter 應用的元資料。這等同於 React Native 中的 package.json 檔案。
資源和資產存放在哪裡,該如何使用?
#Flutter 的資源或資產是隨應用打包部署並在執行時可訪問的檔案。Flutter 應用可包含以下型別的資產:
- 靜態資料,如 JSON 檔案
- 配置檔案
- 圖示和圖片(JPEG、PNG、GIF、動態 GIF、WebP、動態 WebP、BMP 和 WBMP)
Flutter 使用位於專案根目錄的 pubspec.yaml 檔案來標識應用所需的資產。
flutter:
assets:
- assets/my_icon.png
- assets/background.png
assets 子部分指定了應包含在應用中的檔案。每個資產由相對於 pubspec.yaml 檔案的明確路徑標識。宣告資產的順序無關緊要。實際使用的目錄名稱(在本例中為 assets)並不重要。然而,雖然資產可以放在任何應用目錄中,但最佳實踐是將它們放在 assets 目錄中。
在構建過程中,Flutter 會將資產放入一個稱為“資產包”的特殊存檔中,應用在執行時從中讀取。當在 pubspec.yaml 的 assets 部分中指定資產路徑時,構建過程會查詢相鄰子目錄中任何同名檔案。這些檔案也會連同指定的資產一起包含在資產包中。Flutter 在為您的應用選擇適合解析度的圖片時會使用資產變體。
在 React Native 中,您可以透過將圖片檔案放在原始碼目錄並引用它來新增靜態圖片。
<Image source={require('./my-icon.png')} />
// OR
<Image
source={{
url: 'https://native.react.com.tw/img/tiny_logo.png'
}}
/>
在 Flutter 中,透過在元件的 build 方法中使用 Image.asset 建構函式將靜態圖片新增到應用中。
Image.asset('assets/background.png');
有關詳細資訊,請參閱 在 Flutter 中新增資產和圖片。
如何載入網路圖片?
#在 React Native 中,您會在 Image 元件的 source 屬性中指定 uri,並根據需要提供大小。
在 Flutter 中,使用 Image.network 建構函式來包含來自 URL 的圖片。
Image.network('https://docs.flutter.club.tw/assets/images/docs/owl.jpg');
如何安裝包和外掛?
#Flutter 支援使用由其他開發者為 Flutter 和 Dart 生態系統貢獻的共享包。這使您能夠快速構建應用,而無需從頭開發所有內容。包含平臺特定程式碼的包被稱為外掛。
在 React Native 中,您會在命令列中使用 yarn add {package-name} 或 npm install --save {package-name} 來安裝包。
在 Flutter 中,請按以下說明安裝包:
- 要將
google_sign_in包新增為依賴項,請執行flutter pub add。
flutter pub add google_sign_in
- 透過命令列使用
flutter pub get安裝該包。如果您使用的是 IDE,它通常會為您自動執行flutter pub get,或者提示您這樣做。 - 如下所示將包匯入您的應用程式碼中。
import 'package:flutter/material.dart';
您可以在 pub.dev 的 Flutter 包 部分中找到許多由 Flutter 開發者共享的包。
Flutter 元件
#在 Flutter 中,您可以使用描述其檢視在當前配置和狀態下應呈現樣式的元件來構建 UI。
元件通常由許多小的、單一用途的元件組合而成,這些元件巢狀在一起產生強大的效果。例如,Container 元件包含多個負責佈局、繪製、定位和調整大小的元件。具體來說,Container 元件包括 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform 元件。與其透過繼承 Container 來產生自定義效果,不如以新的獨特方式組合這些簡單的元件。
Center 元件是您如何控制佈局的另一個例子。要將元件居中,將其包裹在 Center 元件中,然後使用佈局元件進行對齊、行、列和網格排列。這些佈局元件本身沒有視覺表現。相反,它們唯一的目的是控制其他元件佈局的某一方面。瞭解元件為何以某種方式渲染時,檢查相鄰元件通常很有幫助。
有關更多資訊,請參閱 Flutter 技術概覽。
有關 Widgets 包中核心元件的更多資訊,請參閱 Flutter 基礎元件、Flutter 元件目錄 或 Flutter 元件索引。
檢視
#View 容器的等價物是什麼?
#
在 React Native 中,View 是一個支援 Flexbox 佈局、樣式、觸控處理和可訪問性控制的容器。
在 Flutter 中,您可以使用 Widgets 庫中的核心佈局元件,例如 Container、Column、Row 和 Center。有關更多資訊,請參閱 佈局元件 目錄。
FlatList 或 SectionList 的等價物是什麼?
#
List 是一個垂直排列的滾動元件列表。
在 React Native 中,使用 FlatList 或 SectionList 渲染簡單或分段列表。
// React Native
<FlatList
data={[ ... ]}
renderItem={({ item }) => <Text>{item.key}</Text>}
/>
ListView 是 Flutter 最常用的滾動元件。預設建構函式接受顯式的子元件列表。ListView 最適用於少量元件。對於大型或無限列表,請使用 ListView.builder,它會按需構建子元件,並且只構建可見的子元件。
var data = ['Hello', 'World'];
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return Text(data[index]);
},
);
要學習如何實現無限滾動列表,請參閱官方 infinite_list 示例。
如何使用 Canvas 進行繪製?
#在 React Native 中,沒有內建 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,並將其傳遞給 CustomPaint 的 painter 屬性。CustomPaint 子類必須實現 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()));
}
}
佈局
#如何使用元件定義佈局屬性?
#在 React Native 中,大部分佈局可以透過傳遞給特定元件的 props 來完成。例如,您可以使用 View 元件上的 style 屬性來指定 flexbox 屬性。若要按列排列元件,您可以指定類似 flexDirection: 'column' 的屬性。
// React Native
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
在 Flutter 中,佈局主要由專門設計用於提供佈局的元件,結合控制組件及其樣式屬性來定義。
例如,Column 和 Row 元件接受一組子元件,並分別垂直和水平對齊它們。Container 元件結合了佈局和樣式屬性,而 Center 元件則將其子元件居中。
@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 在其核心元件庫中提供了多種佈局元件。例如 Padding、Align 和 Stack。
有關完整列表,請參閱 佈局元件。
如何對元件進行層疊佈局?
#在 React Native 中,可以使用 absolute 定位對元件進行層疊。
Flutter 使用 Stack 元件按層級排列子元件。元件可以完全或部分重疊在基礎元件之上。
Stack 元件相對於其盒子的邊緣定位其子元件。如果您只想簡單地重疊多個子元件,此類非常有用。
@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 類文件。
樣式
#如何設定元件樣式?
#在 React Native 中,內聯樣式和 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 元件的 style 屬性可以接收 TextStyle 類。如果您想在多處使用相同的文字樣式,可以建立一個 TextStyle 類並將其用於多個 Text 元件。
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?
#
React Native 不包含對圖示的支援,因此使用第三方庫。
在 Flutter 中,匯入 Material 庫也會引入豐富的 Material 圖示 和 顏色。
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 風格) 包為當前的 iOS 設計語言提供了高保真的元件。要使用 CupertinoIcons 字型,請在專案的 pubspec.yaml 檔案中新增 cupertino_icons 依賴項。
name: my_awesome_application
dependencies:
cupertino_icons: ^1.0.8
要全域性自定義元件的顏色和樣式,請使用 ThemeData 為主題的各個方面指定預設顏色。將 MaterialApp 中的 theme 屬性設定為 ThemeData 物件。Colors 類提供了來自 Material Design 調色盤 的顏色。
以下示例將配色方案種子設為 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 元件的 theme 屬性,可以為幾乎所有內容建立統一的樣式。
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: Colors.cyan, brightness: Brightness.dark),
home: const StylingPage(),
);
}
即使不使用 MaterialApp 元件,也可以應用 Theme。Theme 元件在其 data 引數中接收 ThemeData,並將該 ThemeData 應用於其所有子元件。
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(primaryColor: Colors.cyan, brightness: brightness),
child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
//...
),
);
}
狀態管理
#狀態是在元件構建時可同步讀取的資訊,或者是在元件生命週期內可能發生變化的資訊。要在 Flutter 中管理應用狀態,請使用配合 State 物件使用的 StatefulWidget。
有關在 Flutter 中管理狀態方法的更多資訊,請參閱 狀態管理。
StatelessWidget(無狀態元件)
#Flutter 中的 StatelessWidget 是不需要狀態更改的元件——它沒有需要管理的內部狀態。
當您描述的使用者介面部分不依賴於物件本身配置資訊和元件填充時的 BuildContext 以外的任何內容時,無狀態元件非常有用。
AboutDialog、CircleAvatar 和 Text 都是繼承自 StatelessWidget 的無狀態元件示例。
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 類的建構函式來傳遞被標記為 final 的 text。該類繼承自 StatelessWidget——它包含不可變資料。
無狀態元件的 build 方法通常僅在以下三種情況下被呼叫:
- 當元件被插入樹中時
- 當元件的父元件更改其配置時
- 當其依賴的
InheritedWidget發生更改時
StatefulWidget(有狀態元件)
#StatefulWidget 是一個會更改狀態的元件。使用 setState 方法來管理 StatefulWidget 的狀態更改。呼叫 setState() 會告訴 Flutter 框架狀態發生了變化,這會導致應用重新執行 build() 方法,從而反映更改。
狀態是在構建元件時可同步讀取且在元件生命週期內可能發生變化的資訊。確保狀態物件在狀態更改時得到及時通知是元件實現者的責任。當元件可以動態更改時,請使用 StatefulWidget。例如,元件狀態因在表單中輸入、移動滑塊而改變,或者它隨時間推移而改變——例如資料流更新了 UI。
Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是繼承自 StatefulWidget 的有狀態元件示例。
以下示例聲明瞭一個需要 createState() 方法的 StatefulWidget。該方法建立管理元件狀態的狀態物件:_MyStatefulWidgetState。
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key, required this.title});
final String title;
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
下面的狀態類 _MyStatefulWidgetState 實現了元件的 build() 方法。當狀態發生變化(例如,使用者切換按鈕)時,會呼叫帶有新切換值的 setState()。這會導致框架重建 UI 中的此元件。
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 的最佳實踐是什麼?
#設計元件時,請考慮以下幾點:
- 確定元件應該是
StatefulWidget還是StatelessWidget。
在 Flutter 中,元件是有狀態還是無狀態,取決於它們是否依賴狀態更改。
- 如果元件會更改——使用者與其互動或資料流中斷了 UI,那麼它是 Stateful。
- 如果元件是 final 或不可變的,那麼它是 Stateless。
- 確定哪個物件管理元件的狀態(針對
StatefulWidget)。
在 Flutter 中,管理狀態有三種主要方式:
- 元件管理自己的狀態
- 父元件管理元件的狀態
- 混合搭配的方法
決定使用哪種方法時,請考慮以下原則:
- 如果涉及的狀態是使用者資料(例如複選框的選中或取消選中狀態,或滑塊的位置),則該狀態最好由父元件管理。
- 如果涉及的狀態是美學相關的(例如動畫),則元件本身最好管理該狀態。
- 如果不確定,讓父元件管理子元件的狀態。
- 繼承 StatefulWidget 和 State。
MyStatefulWidget 類管理其自身狀態——它繼承自 StatefulWidget,覆蓋 createState() 方法以建立 State 物件,框架呼叫 createState() 來構建元件。在此示例中,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 新增到元件樹中。
在應用的構建方法中將自定義的 StatefulWidget 新增到元件樹中。
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(屬性)
#在 React Native 中,大多陣列件在建立時可以透過不同的引數或屬性(稱為 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,為簡單資料提供持久儲存。
要將 shared_preferences 包新增為依賴項,請執行 flutter pub add。
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 中,螢幕只是元件!要導航到 Flutter 中的新螢幕,請使用 Navigator 元件。
如何在頁面間導航?
#在 React Native 中,有三個主要導航器: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 中,有兩個主要元件用於在螢幕之間導航:
Route(路由)是應用螢幕或頁面的抽象。Navigator(導航器)是一個管理路由的元件。
Navigator 定義為一個以堆疊方式管理一組子元件的元件。導航器管理一組 Route 物件,並提供管理該堆疊的方法,如 Navigator.push 和 Navigator.pop。可以在 MaterialApp 元件中指定路由列表,也可以動態構建它們,例如在英雄動畫(Hero animations)中。以下示例在 MaterialApp 元件中指定了命名路由。
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(元件樹中元件位置的控制代碼)。路由名稱被傳遞給 pushNamed 函式以導航到指定路由。
Navigator.of(context).pushNamed('/a');
您還可以使用 Navigator 的 push 方法,將給定的 Route 新增到緊鄰所提供 BuildContext 的導航器的歷史記錄中,並轉換到它。在以下示例中,MaterialPageRoute 元件是一個模態路由,它用平臺自適應過渡替換整個螢幕。它接受 WidgetBuilder 作為必需引數。
Navigator.push(
context,
MaterialPageRoute<void>(builder: (context) => const UsualNavScreen()),
);
如何使用標籤頁導航(Tabs)和側邊欄導航(Drawer)?
#在 Material Design 應用中,Flutter 導航有兩種主要選擇:標籤頁和抽屜。當沒有足夠的空間支援標籤頁時,抽屜是一個不錯的替代方案。
標籤頁導航
#在 React Native 中,使用 createBottomTabNavigator 和 TabNavigation 來顯示標籤頁並進行標籤頁導航。
// React Native
import { createBottomTabNavigator } from 'react-navigation';
const MyApp = TabNavigator(
{ Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
{ tabBarOptions: { activeTintColor: '#e91e63' } }
);
Flutter 提供了幾種用於抽屜和標籤頁導航的專用元件:
-
TabController -
協調
TabBar和TabBarView之間的標籤頁選擇。 TabBar顯示一橫排標籤頁。
Tab建立一個 Material Design 標籤頁。
TabBarView顯示對應當前選中標籤頁的元件。
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
late TabController controller = TabController(length: 2, vsync: this);
@override
Widget build(BuildContext context) {
return TabBar(
controller: controller,
tabs: const <Tab>[
Tab(icon: Icon(Icons.person)),
Tab(icon: Icon(Icons.email)),
],
);
}
}
需要 TabController 來協調 TabBar 和 TabBarView 之間的標籤頁選擇。TabController 建構函式的 length 引數是標籤頁的總數。需要一個 TickerProvider 在每幀觸發狀態更改時傳送通知。TickerProvider 即 vsync。每當您建立新的 TabController 時,請將 vsync: this 引數傳遞給 TabController 建構函式。
TickerProvider 是由能夠發放 Ticker 物件的類實現的介面。任何需要在幀觸發時得到通知的物件都可以使用 Ticker,但它們最常見的是透過 AnimationController 間接使用。AnimationController 需要 TickerProvider 來獲取其 Ticker。如果您正在從 State 建立 AnimationController,則可以使用 TickerProviderStateMixin 或 SingleTickerProviderStateMixin 類來獲取合適的 TickerProvider。
Scaffold 元件包裹了一個新的 TabBar 元件並建立了兩個標籤頁。TabBarView 元件作為 Scaffold 元件的 body 引數傳遞。對應 TabBar 元件標籤頁的所有螢幕都是 TabBarView 元件的子項,並使用相同的 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()],
),
);
}
}
抽屜導航
#在 React Native 中,匯入所需的 react-navigation 包,然後使用 createDrawerNavigator 和 DrawerNavigation。
// React Native
export default (MyApp1 = DrawerNavigator({
Home: {
screen: SimpleApp
},
Screen2: {
screen: drawerScreen
}
}));
在 Flutter 中,我們可以將 Drawer 元件與 Scaffold 結合使用,建立具有 Material Design 抽屜的佈局。要將 Drawer 新增到應用,請將其包裹在 Scaffold 元件中。Scaffold 元件為遵循 Material Design 指南的應用提供了統一的視覺結構。它還支援特殊的 Material Design 元件,如 Drawers、AppBars 和 SnackBars。
Drawer 元件是一個 Material Design 面板,從 Scaffold 的邊緣水平滑入,以顯示應用中的導航連結。您可以提供 ElevatedButton、Text 元件或一系列專案作為 Drawer 元件的子項顯示。在以下示例中,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 中有抽屜可用時,它會自動顯示一個合適的 IconButton 來展示 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 中的手勢系統有兩個獨立的層級。第一層包括原始指標事件,描述指標(如觸控、滑鼠和手寫筆移動)在螢幕上的位置和移動。第二層包括描述語義動作的手勢,由一個或多個指標移動組成。
如何給元件新增點選事件監聽器?
#在 React Native 中,監聽器使用 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 中,要給元件新增點選(或按下)監聽器,請使用具有 onPress: 欄位的按鈕或可觸控元件。或者,透過將任何元件包裹在 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 呼叫獲取資料?
#React Native 提供用於網路的 Fetch API——您發出 fetch 請求,然後接收響應以獲取資料。
// React Native
const [ipAddress, setIpAddress] = useState('')
const _getIPAddress = () => {
fetch('https://httpbin.org/ip')
.then(response => response.json())
.then(responseJson => {
setIpAddress(responseJson.origin);
})
.catch(error => {
console.error(error);
});
};
Flutter 使用 http 包。
要將 http 包新增為依賴項,請執行 flutter pub add。
flutter pub add http
Flutter 使用 dart:io 核心 HTTP 支援客戶端。要建立 HTTP 客戶端,請匯入 dart:io。
import 'dart:io';
該客戶端支援以下 HTTP 操作:GET、POST、PUT 和 DELETE。
final Uri url = Uri.parse('https://httpbin.org/ip');
final HttpClient 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;
});
}
表單輸入
#文字欄位允許使用者在您的應用中輸入文字,因此可用於構建表單、訊息應用、搜尋體驗等。Flutter 提供了兩個核心文字欄位元件:TextField 和 TextFormField。
如何使用文字輸入框元件?
#在 React Native 中,要輸入文字,您使用 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 元件。每當修改文字欄位時,控制器都會通知其監聽器。
監聽器讀取文字和選擇屬性以瞭解使用者在欄位中輸入的內容。您可以透過控制器的 text 屬性訪問 TextField 中的文字。
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 元件實現的,並且透過 TextEditingController 的 text 屬性訪問來自 TextField 的文字。
如何使用表單元件?
#在 Flutter 中,使用 Form 元件,其中 TextFormField 元件和提交按鈕作為子項傳遞。TextFormField 元件有一個名為 onSaved 的引數,它接受一個回撥函式,並在表單儲存時執行。FormState 物件用於儲存、重置或驗證作為此 Form 後代的每個 FormField。要獲取 FormState,您可以使用祖先為 Form 的上下文呼叫 Form.of(),或者將 GlobalKey 傳遞給 Form 建構函式並呼叫 GlobalKey.currentState()。
@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'),
);
},
);
}
}
平臺特定程式碼
#構建跨平臺應用時,您希望儘可能多地跨平臺複用程式碼。然而,可能會出現程式碼需要根據作業系統而不同的場景。這需要透過宣告特定的平臺來進行單獨實現。
在 React Native 中,將使用以下實現:
// 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 應用。
DevTools 支援效能分析、檢查堆、檢查 widget 樹、日誌診斷、除錯、觀察已執行程式碼行、除錯記憶體洩漏和記憶體碎片。有關更多資訊,請檢視 DevTools 文件。
如果您正在使用 IDE,可以使用 IDE 的偵錯程式除錯應用。
如何執行熱過載(Hot Reload)?
#Flutter 的有狀態熱過載 (Stateful Hot Reload) 功能可幫助您快速輕鬆地進行實驗、構建 UI、新增功能並修復 Bug。您無需每次更改都重新編譯應用,而是可以立即熱過載。應用會更新以反映您的更改,並保留應用的當前狀態。
首先,從您偏好的 IDE 中啟用自動儲存和儲存時熱過載。
VS Code
將以下內容新增到你的 .vscode/settings.json 檔案中
json "files.autoSave": "afterDelay", "dart.flutterHotReloadOnSave": "all", Android Studio 和 IntelliJ
* 開啟 Settings > Tools > Actions on Save 並選擇 Configure autosave options。 - 勾選 Save files if the IDE is idle for X seconds 選項。 - 推薦:設定較小的延遲時間。例如,2 秒。
* 開啟 Settings > Languages & Frameworks > Flutter。 - 勾選 Perform hot reload on save 選項。
在 React Native 中,快捷鍵是 iOS 模擬器的 ⌘R,Android 模擬器則是雙擊 R。
在 Flutter 中,如果您使用的是 IntelliJ IDE 或 Android Studio,可以選擇 Save All (⌘s/ctrl-s),或者單擊工具欄上的 Hot Reload 按鈕。如果您使用 flutter run 在命令列執行應用,請在終端視窗中鍵入 r。您還可以透過在終端視窗中鍵入 R 來執行完全重啟。
如何開啟應用內的開發者選單?
#在 React Native 中,可以透過搖動裝置訪問開發者選單: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 |
| 儲存截圖為 flutter.png | s |
|
| 退出 | q |
動畫
#精心設計的動畫使 UI 感覺直觀,有助於打造精緻的應用外觀,並提升使用者體驗。Flutter 的動畫支援使得實現簡單和複雜的動畫變得容易。Flutter SDK 包含許多帶有標準運動效果的 Material Design 元件,您可以輕鬆自定義這些效果來個性化您的應用。
在 React Native 中,使用 Animated API 建立動畫。
在 Flutter 中,使用 Animation 類和 AnimationController 類。Animation 是一個抽象類,瞭解其當前值及其狀態(已完成或已駁回)。AnimationController 類允許您向前或向後播放動畫,或者停止動畫並將其設定為特定值以自定義運動。
如何新增簡單的淡入動畫?
#在下方的 React Native 示例中,使用 Animated API 建立了一個動畫元件 FadeInView。定義了初始透明度狀態、最終狀態以及過渡發生的持續時間。動畫元件被新增到 Animated 元件內部,透明度狀態 fadeAnim 對映到我們想要設定動畫的 Text 元件的透明度,然後呼叫 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 中建立相同的動畫,請建立一個名為 controller 的 AnimationController 物件並指定持續時間。預設情況下,AnimationController 在給定的持續時間內線性產生從 0.0 到 1.0 的值。每當執行應用的裝置準備好顯示新幀時,動畫控制器就會生成一個新值。通常,這個速率約為每秒 60 個值。
定義 AnimationController 時,必須傳入 vsync 物件。vsync 的存在防止螢幕外動畫消耗不必要的資源。您可以將有狀態物件用作 vsync,方法是將 TickerProviderStateMixin 新增到類定義中。AnimationController 需要一個 TickerProvider,它使用建構函式上的 vsync 引數進行配置。
Tween 描述了開始值和結束值之間的插值,或從輸入範圍到輸出範圍的對映。要將 Tween 物件與動畫一起使用,請呼叫 Tween 物件的 animate() 方法,並傳入您想要修改的 Animation 物件。
在此示例中,使用了 FadeTransition 元件,並將 opacity 屬性對映到 animation 物件。
要開始動畫,請使用 controller.forward()。其他操作也可以使用控制器執行,例如 fling() 或 repeat()。在此示例中,FadeTransition 元件內部使用了 FlutterLogo 元件。
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()),
);
}
}
如何給卡片新增滑動動畫?
#在 React Native 中,使用 PanResponder 或第三方庫來實現滑動動畫。
在 Flutter 中,要新增滑動動畫,請使用 Dismissible 元件並巢狀子元件。
return Dismissible(
key: Key(widget.key.toString()),
onDismissed: (dismissDirection) {
cards.removeLast();
},
child: Container(
//...
),
);
React Native 與 Flutter 元件對照表
#下表列出了常用 React Native 元件與相應的 Flutter 元件及其常見屬性的對照。
| React Native 元件 | Flutter 元件 | 描述 |
|---|---|---|
Button |
凸起按鈕
|
一個基本的凸起按鈕。 |
| onPressed [必需] | 按鈕被點選或以其他方式啟用時的回撥。 | |
| Child | 按鈕的標籤。 | |
Button |
文字按鈕 |
一個基本的扁平按鈕。 |
| onPressed [必需] | 按鈕被點選或以其他方式啟用時的回撥。 | |
| Child | 按鈕的標籤。 | |
ScrollView |
ListView |
線性排列的元件的可滾動列表。 |
| children | ( <Widget> [ ]) 要顯示的子元件列表。 | |
| controller | [ ScrollController ] 可用於控制可滾動元件的物件。 |
|
| itemExtent | [ double ] 如果非 null,則強制子元件在滾動方向上具有給定的範圍。 | |
| scroll Direction | [ Axis ] 滾動檢視滾動的軸。 |
|
FlatList |
ListView.builder
|
用於建立按需構建的線性元件陣列的建構函式。 |
| itemBuilder [必需] | [ IndexedWidgetBuilder ] 有助於按需構建子元件。此回撥僅在索引大於等於 0 且小於 itemCount 時被呼叫。 |
|
| itemCount | [ int ] 提高 ListView 估計最大滾動範圍的能力。 |
|
Image |
Image |
顯示圖片的元件。 |
| image [必需] | 要顯示的圖片。 | |
| Image. asset | 為指定圖片的不同方式提供了多個建構函式。 | |
| width, height, color, alignment | 圖片的樣式和佈局。 | |
| fit | 將圖片插入佈局期間分配的空間中。 | |
Modal |
ModalRoute |
一種阻止與之前路由互動的路由。 |
| animation | 驅動路由過渡和前一個路由前進過渡的動畫。 | |
ActivityIndicator |
CircularProgressIndicator
|
沿圓圈顯示進度的元件。 |
| strokeWidth | 用於繪製圓圈的線條寬度。 | |
| backgroundColor | 進度指示器的背景顏色。預設情況下為當前主題的 ThemeData.backgroundColor。 |
|
ActivityIndicator |
LinearProgressIndicator
|
沿線條顯示進度的元件。 |
| value | 此進度指示器的值。 | |
RefreshControl |
RefreshIndicator
|
支援 Material "下拉重新整理" 習慣用法的元件。 |
| color | 進度指示器的前景色。 | |
| onRefresh | 當用戶將重新整理指示器拖動到足以證明他們希望應用重新整理時呼叫的函式。 | |
View |
容器 |
包裹子元件的元件。 |
View |
Column |
垂直排列其子元件的元件。 |
View |
Row |
水平排列其子元件的元件。 |
View |
Center |
將其子元件在其內部居中的元件。 |
View |
Padding |
透過給定的內邊距插入其子元件的元件。 |
| padding [必需] | [ EdgeInsets ] 插入子元件的空間量。 | |
TouchableOpacity |
GestureDetector
|
檢測手勢的元件。 |
| onTap | 點擊發生時的回撥。 | |
| onDoubleTap | 快速連續在同一位置點選兩次時的回撥。 | |
TextInput |
TextInput |
系統文字輸入控制元件的介面。 |
| controller | [ TextEditingController ] 用於訪問和修改文字。 |
|
文字 |
文字 |
顯示具有單一樣式的字串文字的 Text 元件。 |
| data | [ String ] 要顯示的文字。 | |
| textDirection | [ TextAlign ] 文字流動的方向。 |
|
Switch |
Switch |
Material Design 開關。 |
| value [必需] | [ boolean ] 此開關是開啟還是關閉。 | |
| onChanged [必需] | [ callback ] 當用戶切換開關開啟或關閉時呼叫。 |