給 React Native 開發人員的 Flutter
- JavaScript 開發人員 (ES6) 的 Dart 簡介
- 基本概念
- 專案結構和資源
- Flutter 小工具 (Widget)
- 視圖 (Views)
- 版面配置 (Layouts)
- 樣式設定 (Styling)
- 狀態管理
- Props
- 本機儲存空間
- 路由 (Routing)
- 手勢偵測和觸控事件處理
- 發出 HTTP 網路請求
- 表單輸入
- 平台特定程式碼
- 偵錯
- 動畫 (Animation)
- React Native 和 Flutter 小工具等效元件
這份文件適用於希望運用現有 RN 知識來使用 Flutter 建置行動應用程式的 React Native (RN) 開發人員。如果您了解 RN 框架的基本原理,則可以使用這份文件作為開始學習 Flutter 開發的方式。
這份文件可以當作食譜使用,您可以跳過章節並尋找與您的需求最相關的問題。
JavaScript 開發人員 (ES6) 的 Dart 簡介
#與 React Native 一樣,Flutter 使用反應式視圖。但是,雖然 RN 會轉譯為原生小工具,但 Flutter 會完全編譯為原生程式碼。Flutter 控制螢幕上的每個像素,這避免了因需要 JavaScript 橋接器而導致的效能問題。
Dart 是一種容易學習的語言,並提供以下功能
- 為建置網頁、伺服器和行動應用程式提供開放原始碼、可擴充的程式設計語言。
- 提供一種物件導向、單一繼承的語言,該語言使用 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
會被視為 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
#與 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 入門文件。
如何匯入小工具?
#在 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 中,幾乎所有東西都是小工具。
小工具是應用程式使用者介面的基本建置區塊。您將小工具組成階層結構,稱為小工具樹狀結構。每個小工具都會巢狀於父小工具內,並從其父小工具繼承屬性。即使是應用程式物件本身也是小工具。沒有單獨的「應用程式」物件。而是由根小工具扮演此角色。
小工具可以定義
- 結構元素—例如按鈕或功能表
- 樣式元素—例如字型或配色
- 版面配置的方面—例如邊框間距或對齊方式
以下範例顯示使用 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!」。您可以免費獲得比基本「Hello world!」應用程式更多的功能。
撰寫應用程式時,您將使用兩種小工具:StatelessWidget
或 StatefulWidget
。StatelessWidget
就是字面上的意思—沒有狀態的小工具。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 - 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.
我應該將資源和資產放在哪裡,以及如何使用它們?
#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
的資源部分中指定資源路徑時,建置過程會尋找相鄰子目錄中任何具有相同名稱的檔案。這些檔案也會與指定的資源一起包含在資源包中。當為您的應用程式選擇適當解析度的圖片時,Flutter 會使用資源變體。
在 React Native 中,您會將圖片檔案放置在原始碼目錄中並參考它,來新增靜態圖片。
<Image source={require('./my-icon.png')} />
// OR
<Image
source={{
url: 'https://reactnative.dev.org.tw/img/tiny_logo.png'
}}
/>
在 Flutter 中,在 Widget 的 build 方法中使用 Image.asset
建構函式,將靜態圖片新增至您的應用程式。
Image.asset('assets/background.png');
如需更多資訊,請參閱在 Flutter 中新增資源和圖片。
如何透過網路載入影像?
#在 React Native 中,您會在 Image
元件的 source
屬性中指定 uri
,並在需要時提供大小。
在 Flutter 中,使用 Image.network
建構函式從 URL 包含圖片。
Image.network('https://flutter-docs.dev.org.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 小工具 (Widget)
#在 Flutter 中,您可以使用 Widget 建置您的 UI,這些 Widget 描述了在目前配置和狀態下視圖應有的外觀。
Widget 通常由許多小型、單用途的 Widget 組成,這些 Widget 會巢狀配置以產生強大的效果。例如,Container
Widget 由多個負責配置、繪製、定位和調整大小的 Widget 組成。具體來說,Container
Widget 包含 LimitedBox
、ConstrainedBox
、Align
、Padding
、DecoratedBox
和 Transform
Widget。您可以使用這些和其他簡單的 Widget 以新的獨特方式組合,而無需子類化 Container
以產生自訂效果。
Center
Widget 是另一個如何控制配置的範例。若要置中 Widget,請將其包裝在 Center
Widget 中,然後使用配置 Widget 進行對齊、行、欄和網格。這些配置 Widget 本身沒有視覺表示。相反地,它們的唯一目的是控制另一個 Widget 配置的某些方面。若要了解 Widget 以某種方式呈現的原因,檢查相鄰的 Widget 通常會很有幫助。
如需更多資訊,請參閱Flutter 技術概述。
如需有關 Widgets
套件中核心 Widget 的更多資訊,請參閱Flutter 基本 Widget、Flutter Widget 目錄或Flutter Widget 索引。
視圖 (Views)
#View
容器的等效項目是什麼?
#在 React Native 中,View
是一個容器,支援使用 Flexbox
進行配置、樣式、觸控處理和輔助功能控制。
在 Flutter 中,您可以使用 Widgets
程式庫中的核心配置 Widget,例如 Container
、Column
、Row
和 Center
。如需更多資訊,請參閱配置 Widget目錄。
FlatList
或 SectionList
的等效項目是什麼?
#List
是垂直排列的元件的可捲動清單。
在 React Native 中,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 繪圖或著色?
#在 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
類別來繪製到 canvas。
以下範例顯示如何使用 CustomPaint
Widget 在繪製階段進行繪製。它實作了抽象類別 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()),
);
}
}
版面配置 (Layouts)
#如何使用小工具定義版面配置屬性?
#在 React Native 中,大部分配置可以使用傳遞給特定元件的屬性來完成。例如,您可以使用 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。
如何將小工具分層?
#在 React Native 中,元件可以使用 absolute
定位來分層。
Flutter 使用 Stack
Widget 以圖層方式排列子項 Widget。這些 Widget 可以完全或部分重疊基準 Widget。
Stack
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
類別文件。
樣式設定 (Styling)
#如何設定元件樣式?
#在 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
Widget 可以採用 TextStyle
類別作為其 style 屬性。如果您想在多個位置使用相同的文字樣式,您可以建立 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
?
#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 設計語言提供高保真 Widget。若要使用 CupertinoIcons
字型,請在您專案的 pubspec.yaml
檔案中新增 cupertino_icons
的相依性。
name: my_awesome_application
dependencies:
cupertino_icons: ^1.0.8
若要全域自訂元件的色彩和樣式,請使用 ThemeData
來指定主題各個方面的預設色彩。將 MaterialApp
中的 theme 屬性設定為 ThemeData
物件。Colors
類別提供 Material Design 調色盤中的色彩。
以下範例會將色彩配置從 seed 設定為 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
Widget 的 theme 屬性,為幾乎所有內容建立統一的樣式。
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.cyan,
brightness: Brightness.dark,
),
home: const StylingPage(),
);
}
即使不使用 MaterialApp
Widget,也可以套用 Theme
。Theme
Widget 會在其 data
參數中採用 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 中管理應用程式狀態,請使用與 State 物件配對的 StatefulWidget
。
如需在 Flutter 中管理狀態方法的更多資訊,請參閱狀態管理。
StatelessWidget
#在 Flutter 中,StatelessWidget
是一個不需要狀態變更的 Widget — 它沒有需要管理的內部狀態。
當您描述的使用者介面部分,不依賴於物件本身的組態資訊以及 Widget 膨脹所在的 BuildContext
時,無狀態 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
類別的建構函式來傳遞標記為 final
的 text
。這個類別繼承了 StatelessWidget
— 它包含不可變的資料。
無狀態 Widget 的 build
方法通常只會在三種情況下被呼叫
- 當 Widget 被插入到樹狀結構中時
- 當 Widget 的父級變更其組態時
- 當它所依賴的
InheritedWidget
發生變更時
StatefulWidget
#StatefulWidget
是一個會變更狀態的 Widget。使用 setState
方法來管理 StatefulWidget
的狀態變更。呼叫 setState()
會告知 Flutter 框架狀態中發生了一些變更,這會導致應用程式重新執行 build()
方法,以便應用程式可以反映變更。
狀態 是指在建構 Widget 時可以同步讀取,並且可能會在 Widget 生命週期中變更的資訊。Widget 實作者有責任確保狀態變更時,立即通知狀態物件。當 Widget 可以動態變更時,請使用 StatefulWidget
。例如,Widget 的狀態會因在表單中輸入文字或移動滑桿而變更。或者,它可能會隨著時間推移而變更 — 例如,資料饋送更新 UI。
Checkbox
、Radio
、Slider
、InkWell
、Form
和 TextField
都是繼承 StatefulWidget
的有狀態 Widget 範例。
以下範例宣告了一個需要 createState()
方法的 StatefulWidget
。此方法會建立管理 Widget 狀態的狀態物件 _MyStatefulWidgetState
。
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({
super.key,
required this.title,
});
final String title;
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
以下狀態類別 _MyStatefulWidgetState
實作了 Widget 的 build()
方法。當狀態變更時,例如,當使用者切換按鈕時,會使用新的切換值呼叫 setState()
。這會導致框架在 UI 中重建此 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 時要考慮的一些事項。
- 判斷 Widget 應該是
StatefulWidget
還是StatelessWidget
。
在 Flutter 中,Widget 要么是有狀態的,要么是無狀態的 — 這取決於它們是否依賴狀態變更。
- 如果 Widget 會變更 — 使用者與其互動或資料饋送中斷 UI,則它是有狀態的。
- 如果 Widget 是最終的或不可變的,則它是無狀態的。
- 判斷哪個物件管理 Widget 的狀態 (針對
StatefulWidget
)。
在 Flutter 中,有三種主要的方式來管理狀態
- Widget 管理自己的狀態
- 父級 Widget 管理 Widget 的狀態
- 混合搭配方法
在決定使用哪種方法時,請考慮以下原則
- 如果相關的狀態是使用者資料,例如核取方塊的勾選或取消勾選模式,或滑桿的位置,則最好由父級 Widget 管理該狀態。
- 如果相關的狀態是美觀的,例如動畫,則 Widget 本身最好管理該狀態。
- 如有疑問,請讓父級 Widget 管理子 Widget 的狀態。
- 繼承 StatefulWidget 和 State。
MyStatefulWidget
類別管理自己的狀態 — 它繼承了 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
#在 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
類別提供的設定方法。設定方法適用於各種基本類型,例如 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;
});
}
路由 (Routing)
#大多數應用程式都包含多個畫面,用於顯示不同類型的資訊。例如,您可能會有一個產品畫面,顯示影像,使用者可以在其中點選產品影像,以在新畫面上取得關於該產品的更多資訊。
在 Android 中,新畫面是新的 Activity。在 iOS 中,新畫面是新的 ViewController。在 Flutter 中,畫面只是 Widget!若要在 Flutter 中瀏覽到新畫面,請使用 Navigator Widget。
如何在畫面之間導覽?
#在 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 中,有兩個主要的 Widget 用於在畫面之間導覽
Navigator
定義為一個 Widget,它使用堆疊原則管理一組子 Widget。導覽器管理 Route
物件的堆疊,並提供管理堆疊的方法,例如 Navigator.push
和 Navigator.pop
。路由清單可能會在 MaterialApp
Widget 中指定,或者可能會動態建構,例如,在英雄動畫中。以下範例在 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
新增到最緊密封閉給定 BuildContext
的導覽器的歷程記錄中,並轉換到該路由。在以下範例中,MaterialPageRoute
Widget 是一個模態路由,它會使用平台自適應轉換來取代整個畫面。它會將 WidgetBuilder
作為必要參數。
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UsualNavScreen(),
),
);
如何使用分頁導覽和抽屜導覽?
#在 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 為抽屜和索引標籤導覽提供了幾個專門的 Widget
TabController
- 協調
TabBar
和TabBarView
之間的索引標籤選擇。 TabBar
- 顯示索引標籤的水平列。
Tab
- 建立 Material Design TabBar 索引標籤。
TabBarView
- 顯示與目前選取的索引標籤對應的 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)),
],
);
}
}
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
Widget 會包裝新的 TabBar
Widget 並建立兩個索引標籤。TabBarView
Widget 會作為 Scaffold
Widget 的 body
參數傳遞。與 TabBar
Widget 的索引標籤對應的所有畫面都是 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()],
));
}
}
抽屜導覽
#在 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
中有 Drawer
時,Scaffold
小工具還包含一個 AppBar
小工具,該小工具會自動顯示適當的 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: field
的按鈕或可觸控小工具。或者,將手勢偵測新增至任何小工具,方法是將其包裝在 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 來進行網路連線—您發出提取請求,然後接收回應以取得資料。
// 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 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;
});
}
表單輸入
#文字欄位允許使用者在您的應用程式中輸入文字,以便它們可用於建立表單、訊息應用程式、搜尋體驗等等。 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.of()
,其環境是 Form
的上層,或將 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 包括對效能分析、檢查堆積、檢查小工具樹狀結構、記錄診斷、偵錯、觀察執行的程式碼行、偵錯記憶體洩漏和記憶體片段化的支援。如需詳細資訊,請查看 DevTools 文件。
如果您使用的是 IDE,則可以使用 IDE 的偵錯工具來偵錯您的應用程式。
如何執行熱重載?
#Flutter 的「具有狀態的熱重載」功能可協助您快速且輕鬆地試驗、建置 UI、新增功能和修正錯誤。您不需要每次進行變更時都重新編譯應用程式,而是可以立即熱重載應用程式。應用程式會更新以反映您的變更,並且會保留應用程式的目前狀態。
在 React Native 中,iOS 模擬器的快速鍵是 ⌘R,Android 模擬器則是點擊 R 兩次。
在 Flutter 中,如果您使用的是 IntelliJ IDE 或 Android Studio,則可以選取「全部儲存」(⌘s/ctrl-s),或者您可以按一下工具列上的「熱重載」按鈕。如果您是使用 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 |
動畫 (Animation)
#設計完善的動畫使 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
的存在可防止螢幕外動畫耗用不必要的資源。您可以透過將 TickerProviderStateMixin
新增至類別定義,將您的具狀態物件用作 vsync
。 AnimationController
需要一個 TickerProvider,它會使用建構函式上的 vsync
引數來設定。
Tween
描述起點值和終點值之間的內插法,或從輸入範圍到輸出範圍的對應。若要將 Tween
物件與動畫搭配使用,請呼叫 Tween
物件的 animate()
方法,並將您要修改的 Animation
物件傳遞給它。
在此範例中,使用 FadeTransition
小工具,且 opacity
屬性會對應到 animation
物件。
若要開始動畫,請使用 controller.forward()
。也可以使用控制器執行其他操作,例如 fling()
或 repeat()
。在此範例中,FlutterLogo
小工具會在 FadeTransition
小工具內使用。
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 | ElevatedButton | 基本凸起按鈕。 |
onPressed [必要] | 點擊或以其他方式啟動按鈕時的回呼。 | |
Child | 按鈕的標籤。 | |
Button | TextButton | 基本平面按鈕。 |
onPressed [必要] | 點擊或以其他方式啟動按鈕時的回呼。 | |
Child | 按鈕的標籤。 | |
ScrollView | ListView | 以線性方式排列的可捲動小工具清單。 |
children | ( <Widget> [ ] ) 要顯示的子項小工具清單。 | |
controller | [ ScrollController ] 可用於控制可捲動小工具的物件。 | |
itemExtent | [ double ] 如果為非 Null,則強制子項在捲動方向上具有給定的範圍。 | |
scroll Direction | [ Axis ] 捲動檢視捲動的軸線。 | |
FlatList | ListView.builder | 隨需建立的線性小工具陣列的建構函式。 |
itemBuilder [必要] | [IndexedWidgetBuilder ] 有助於隨需建置子項。此回呼僅在索引大於或等於零且小於 itemCount 時才會呼叫。 | |
itemCount | [ int ] 提升 ListView 估計最大捲動範圍的能力。 | |
Image | Image | 一個顯示圖片的 Widget。 |
image [必要] | 要顯示的圖片。 | |
Image.asset | 針對各種指定圖片的方式,提供了多個建構子。 | |
width、height、color、alignment | 圖片的樣式和佈局。 | |
fit | 將圖片嵌入佈局期間分配的空間中。 | |
Modal | ModalRoute | 一個會阻擋與先前路由互動的路由。 |
animation | 驅動路由轉換以及先前路由向前轉換的動畫。 | |
ActivityIndicator | CircularProgressIndicator | 一個沿著圓形顯示進度的 Widget。 |
strokeWidth | 用於繪製圓形的線條寬度。 | |
backgroundColor | 進度指示器的背景顏色。預設為目前主題的 ThemeData.backgroundColor 。 | |
ActivityIndicator | LinearProgressIndicator | 一個沿著線條顯示進度的 Widget。 |
value | 此進度指示器的值。 | |
RefreshControl | RefreshIndicator | 一個支援 Material「滑動以重新整理」慣用法的 Widget。 |
color | 進度指示器的前景色。 | |
onRefresh | 當使用者將重新整理指示器拖曳到足以顯示他們想要應用程式重新整理時呼叫的函式。 | |
View | Container | 一個環繞子 Widget 的 Widget。 |
View | Column | 一個以垂直陣列顯示其子 Widget 的 Widget。 |
View | Row | 一個以水平陣列顯示其子 Widget 的 Widget。 |
View | Center | 一個將其子 Widget 置於自身中心點的 Widget。 |
View | Padding | 一個使用給定邊距嵌入其子 Widget 的 Widget。 |
padding [必要] | [ EdgeInsets ] 嵌入子 Widget 的空間量。 | |
TouchableOpacity | GestureDetector | 一個偵測手勢的 Widget。 |
onTap | 當發生點擊時的回呼。 | |
onDoubleTap | 當在快速連續的時間內,在相同位置發生兩次點擊時的回呼。 | |
TextInput | TextInput | 系統文字輸入控制項的介面。 |
controller | [ TextEditingController ] 用於存取和修改文字。 | |
文字 | 文字 | Text Widget,以單一樣式顯示文字字串。 |
data | [ String ] 要顯示的文字。 | |
textDirection | [ TextAlign ] 文字流動的方向。 | |
Switch | Switch | 一個 Material Design 開關。 |
value [必要] | [ boolean ] 此開關是否開啟或關閉。 | |
onChanged [必要] | [ callback ] 當使用者切換開關開啟或關閉時呼叫。 | |
Slider | Slider | 用於從值的範圍中選擇。 |
value [必要] | [ double ] 滑桿的目前值。 | |
onChanged [必要] | 當使用者為滑桿選取新值時呼叫。 |
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-08-06。 檢視原始碼 或 回報問題。