給 Android 開發者的 Flutter
- 視圖 (Views)
- 意圖 (Intents)
- 非同步 UI
- 專案結構與資源
- 活動 (Activities) 與片段 (fragments)
- 佈局 (Layouts)
- 手勢偵測與觸控事件處理
- 列表視圖 (Listviews) 與適配器 (adapters)
- 文字處理
- 表單輸入
- Flutter 外掛程式
- 主題
- 資料庫與本地儲存
- 偵錯
- 通知
本文檔旨在為希望將現有的 Android 知識應用於使用 Flutter 建立行動應用程式的 Android 開發人員所設計。如果您了解 Android 框架的基礎知識,則可以使用本文檔作為 Flutter 開發的快速入門。
當您使用 Flutter 建立應用程式時,您的 Android 知識和技能非常寶貴,因為 Flutter 在許多功能和配置方面都依賴行動作業系統。Flutter 是一種為行動裝置建立 UI 的新方式,但它有一個外掛程式系統,可以與 Android (和 iOS) 進行非 UI 任務的通訊。如果您是 Android 專家,則無需重新學習所有內容即可使用 Flutter。
本文檔可用作食譜,您可以跳過章節並找到最符合您需求的問題。
視圖 (Views)
#在 Flutter 中,視圖 (View) 的等價物是什麼?
#在 Android 中,View
是螢幕上顯示的所有內容的基礎。按鈕、工具列和輸入框,一切都是 View。在 Flutter 中,View
的大致等價物是 Widget
。小部件 (Widgets) 並不完全對應到 Android 視圖 (views),但在您熟悉 Flutter 的運作方式時,您可以將它們視為「宣告和建構 UI 的方式」。
但是,它們與 View
有一些差異。首先,小部件 (widgets) 的生命週期不同:它們是不可變的,並且僅存在到需要更改時。每當小部件 (widgets) 或其狀態變更時,Flutter 的框架都會建立一個新的小部件實例樹。相比之下,Android 視圖 (view) 會繪製一次,並且除非呼叫 invalidate
,否則不會重新繪製。
Flutter 的小部件 (widgets) 很輕巧,部分原因是它們的不可變性。因為它們本身不是視圖 (views),並且不會直接繪製任何內容,而是 UI 及其語義的描述,這些描述會在幕後「膨脹」為實際的視圖物件。
Flutter 包含 Material Components 函式庫。這些小部件 (widgets) 實作了 Material Design 指南。Material Design 是一個靈活的設計系統,針對包括 iOS 在內的所有平台進行了最佳化。
但是 Flutter 非常靈活且具有表達力,足以實作任何設計語言。例如,在 iOS 上,您可以使用 Cupertino 小部件 來產生一個看起來像 Apple 的 iOS 設計語言 的介面。
我該如何更新小部件 (widgets)?
#在 Android 中,您可以透過直接修改視圖 (views) 來更新它們。但是,在 Flutter 中,Widget
是不可變的,並且不會直接更新,而是您必須使用小部件 (widget) 的狀態。
這就是 Stateful
和 Stateless
小部件的概念的由來。StatelessWidget
就如其名稱所示 — 一個沒有狀態資訊的小部件。
當您描述的使用者介面部分僅取決於物件中的配置資訊時,StatelessWidgets
會很有用。
例如,在 Android 中,這類似於放置一個包含您標誌的 ImageView
。標誌在執行階段不會變更,因此請在 Flutter 中使用 StatelessWidget
。
如果您想根據發出 HTTP 呼叫或使用者互動後收到的資料來動態變更 UI,則必須使用 StatefulWidget
,並告知 Flutter 框架小部件 (widget) 的 State
已更新,以便它可以更新該小部件。
這裡需要注意的重要一點是,無論是無狀態小部件 (stateless) 還是有狀態小部件 (stateful),其核心行為都相同。它們會在每一幀都重建,不同之處在於 StatefulWidget
有一個 State
物件,可以跨幀儲存狀態資料並還原它。
如果您有疑問,請始終記住此規則:如果小部件 (widget) 變更(例如,由於使用者互動),則它是有狀態的。但是,如果小部件 (widget) 對變更做出反應,則如果它本身不對變更做出反應,則包含的父小部件 (parent widget) 仍然可以是無狀態的。
以下範例說明如何使用 StatelessWidget
。常見的 StatelessWidget
是 Text
小部件。如果您查看 Text
小部件的實作,您會發現它繼承了 StatelessWidget
。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如您所見,Text
小部件沒有與其相關聯的狀態資訊,它僅呈現在其建構子中傳入的內容,僅此而已。
但是,如果您想讓「我喜歡 Flutter」動態變更,例如,在點擊 FloatingActionButton
時呢?
若要達成此目的,請將 Text
小部件包裝在 StatefulWidget
中,並在使用者點擊按鈕時更新它。
例如
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default placeholder text.
String textToShow = 'I Like Flutter';
void _updateText() {
setState(() {
// Update the text.
textToShow = 'Flutter is Awesome!';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
我該如何佈局我的小部件?我的 XML 佈局檔在哪裡?
#在 Android 中,您以 XML 撰寫佈局,但在 Flutter 中,您以小部件樹 (widget tree) 撰寫佈局。
以下範例說明如何顯示帶有邊距的簡單小部件
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.only(left: 20, right: 30),
),
onPressed: () {},
child: const Text('Hello'),
),
),
);
}
您可以在 小部件目錄中查看 Flutter 提供的一些佈局。
我該如何從我的佈局中新增或移除元件?
#在 Android 中,您可以在父層呼叫 addChild()
或 removeChild()
以動態新增或移除子視圖 (child views)。在 Flutter 中,由於小部件 (widgets) 是不可變的,因此沒有直接等同於 addChild()
的方法。相反,您可以將一個返回小部件 (widget) 的函式傳遞給父層,並使用布林標誌來控制該子層的建立。
例如,以下是如何在您點擊 FloatingActionButton
時在兩個小部件 (widgets) 之間切換的方法
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text('Toggle One');
} else {
return ElevatedButton(
onPressed: () {},
child: const Text('Toggle Two'),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
我該如何為小部件製作動畫?
#在 Android 中,您可以使用 XML 建立動畫,或在視圖上呼叫 animate()
方法。在 Flutter 中,使用動畫函式庫,將小部件包裝在動畫小部件內部來製作動畫。
在 Flutter 中,使用 AnimationController
,它是一個 Animation<double>
,可以暫停、尋找、停止和反轉動畫。它需要一個 Ticker
來指示 vsync 何時發生,並在執行時在每一幀產生 0 到 1 之間的線性內插。然後,您建立一個或多個 Animation
,並將它們附加到控制器。
例如,您可以使用 CurvedAnimation
來實作沿著內插曲線的動畫。從這個意義上講,控制器是動畫進度的「主」來源,而 CurvedAnimation
計算取代控制器預設線性運動的曲線。與小部件 (widgets) 一樣,Flutter 中的動畫也使用組合。
在建立小部件樹時,您會將 Animation
指派給小部件的動畫屬性,例如 FadeTransition
的不透明度,並告知控制器開始動畫。
以下範例說明如何在您按下 FloatingActionButton
時編寫一個將小部件淡入標誌的 FadeTransition
import 'package:flutter/material.dart';
void main() {
runApp(const FadeAppTest());
}
class FadeAppTest extends StatelessWidget {
const FadeAppTest({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fade Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
const MyFadeTest({super.key, required this.title});
final String title;
@override
State<MyFadeTest> createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FadeTransition(
opacity: curve,
child: const FlutterLogo(
size: 100,
),
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade',
onPressed: () {
controller.forward();
},
child: const Icon(Icons.brush),
),
);
}
}
如需更多資訊,請參閱 動畫與動態小部件、動畫教學 和 動畫概觀。
我該如何使用畫布 (Canvas) 進行繪製/塗色?
#在 Android 中,您會使用 Canvas
和 Drawable
在螢幕上繪製圖片和形狀。Flutter 也有類似的 Canvas
API,因為它是基於相同的底層渲染引擎 Skia。因此,對於 Android 開發人員來說,在 Flutter 中繪製到畫布是一項非常熟悉的任務。
Flutter 有兩個類別可以幫助您繪製到畫布上:CustomPaint
和 CustomPainter
,後者實作您的演算法以繪製到畫布上。
若要了解如何在 Flutter 中實作簽名繪製器,請參閱 Collin 在 Custom Paint 上的回答。
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
const Signature({super.key});
@override
SignatureState createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child: CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
}
}
@override
bool shouldRepaint(SignaturePainter oldDelegate) =>
oldDelegate.points != points;
}
我該如何建立自定義小部件?
#在 Android 中,您通常會子類別化 View
,或使用預先存在的視圖,來覆寫和實作達到所需行為的方法。
在 Flutter 中,透過組合較小的 widget (而不是擴充它們) 來建立自訂 widget。這有點類似於在 Android 中實作自訂 ViewGroup
,其中所有的建構區塊都已經存在,但您提供不同的行為 — 例如,自訂版面配置邏輯。
例如,您如何建立一個在建構函式中取得標籤的 CustomButton
? 建立一個組合了帶有標籤的 ElevatedButton
的 CustomButton,而不是透過擴充 ElevatedButton
來建立。
class CustomButton extends StatelessWidget {
final String label;
const CustomButton(this.label, {super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(label),
);
}
}
然後使用 CustomButton
,就像您使用任何其他 Flutter widget 一樣
@override
Widget build(BuildContext context) {
return const Center(
child: CustomButton('Hello'),
);
}
意圖 (Intents)
#在 Flutter 中,意圖 (Intent) 的等價物是什麼?
#在 Android 中,Intent
主要有兩個使用案例:在 Activity 之間導覽,以及與元件通訊。另一方面,Flutter 沒有 intent 的概念,儘管您仍然可以透過原生整合 (使用外掛程式) 來啟動 intent。
Flutter 沒有與 Activity 和 Fragment 直接對應的概念;相反地,在 Flutter 中,您可以使用 Navigator
和 Route
在同一個 Activity
中於螢幕之間導覽。
Route
是應用程式的「螢幕」或「頁面」的抽象概念,而 Navigator
是管理路由的 widget。Route 大致對應到一個 Activity
,但不具有相同的意義。Navigator 可以推送和彈出路由,以在螢幕之間移動。Navigator 的運作方式就像一個堆疊,您可以將要導覽的新路由 push()
到堆疊上,並且在您想要「返回」時可以從堆疊中 pop()
路由。
在 Android 中,您會在應用程式的 AndroidManifest.xml
內宣告您的 Activity。
在 Flutter 中,您有幾個選項可以在頁面之間導覽
- 指定路由名稱的
Map
。(使用MaterialApp
) - 直接導覽到路由。(使用
WidgetsApp
)
以下範例建立一個 Map。
void main() {
runApp(MaterialApp(
home: const MyAppHome(), // Becomes the route named '/'.
routes: <String, WidgetBuilder>{
'/a': (context) => const MyPage(title: 'page A'),
'/b': (context) => const MyPage(title: 'page B'),
'/c': (context) => const MyPage(title: 'page C'),
},
));
}
透過將路由名稱 push
到 Navigator
來導覽到路由。
Navigator.of(context).pushNamed('/b');
Intent
的另一個常見使用案例是呼叫外部元件,例如相機或檔案選擇器。為此,您需要建立原生平台整合 (或使用現有的外掛程式)。
若要了解如何建立原生平台整合,請參閱開發套件和外掛程式。
我該如何在 Flutter 中處理來自外部應用程式的傳入意圖?
#Flutter 可以透過直接與 Android 層交談並請求已共用的資料來處理來自 Android 的傳入 intent。
以下範例會在執行我們 Flutter 程式碼的原生 Activity 上註冊文字共用 intent 篩選器,以便其他應用程式可以與我們的 Flutter 應用程式共用文字。
基本流程表示我們首先在 Android 原生端 (在我們的 Activity
中) 處理共用的文字資料,然後等待 Flutter 請求資料,以便使用 MethodChannel
來提供資料。
首先,在 AndroidManifest.xml
中為所有 intent 註冊 intent 篩選器
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- ... -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
然後在 MainActivity
中,處理 intent、從 intent 中擷取已共用的文字,並保留它。當 Flutter 準備好處理時,它會使用平台通道請求資料,並從原生端傳送過來
package com.example.shared;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
private String sharedText;
private static final String CHANNEL = "app.channel.shared.data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent); // Handle text being sent
}
}
}
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
);
}
void handleSendText(Intent intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
}
}
最後,在 widget 渲染時從 Flutter 端請求資料
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample Shared App Handler',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
static const platform = MethodChannel('app.channel.shared.data');
String dataShared = 'No data';
@override
void initState() {
super.initState();
getSharedText();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(dataShared)));
}
Future<void> getSharedText() async {
var sharedData = await platform.invokeMethod('getSharedText');
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
}
startActivityForResult()
的等價物是什麼?
#Navigator
類別處理 Flutter 中的路由,並用於從您已推送到堆疊上的路由取得結果。這是透過 await
等待 push()
傳回的 Future
來完成的。
例如,若要啟動一個讓使用者選取其位置的位置路由,您可以執行下列動作
Object? coordinates = await Navigator.of(context).pushNamed('/location');
然後,在您的位置路由內,一旦使用者選取了他們的位置,您可以使用結果 pop
堆疊
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
非同步 UI
#在 Flutter 中,runOnUiThread()
的等價物是什麼?
#Dart 具有單執行緒的執行模型,並支援 Isolate
(一種在另一個執行緒上執行 Dart 程式碼的方式)、事件迴圈和非同步程式設計。除非您產生一個 Isolate
,否則您的 Dart 程式碼會在主要 UI 執行緒中執行,並由事件迴圈驅動。Flutter 的事件迴圈相當於 Android 的主要 Looper
— 也就是附加到主要執行緒的 Looper
。
Dart 的單執行緒模型並不表示您需要將所有內容都作為導致 UI 凍結的封鎖操作來執行。與 Android 不同,Android 要求您隨時保持主要執行緒空閒,在 Flutter 中,請使用 Dart 語言提供的非同步功能 (例如 async
/await
) 來執行非同步工作。如果您在 C#、Javascript 中使用過 async
/await
範例,或如果您使用過 Kotlin 的協程,您可能會很熟悉 async
/await
範例。
例如,您可以使用 async
/await
並讓 Dart 處理繁重的工作,而不會導致 UI 掛起而執行網路程式碼
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
一旦 await
等待的網路呼叫完成,請呼叫 setState()
來更新 UI,這會觸發 widget 子樹的重建並更新資料。
以下範例會非同步載入資料並在 ListView
中顯示資料
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
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),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
請參閱下一節,以取得更多關於在背景中執行工作以及 Flutter 與 Android 的不同之處的資訊。
你如何將工作移至背景執行緒?
#在 Android 中,當您想要存取網路資源時,通常會移至背景執行緒並執行工作,以免封鎖主要執行緒並避免 ANR。例如,您可能會使用 AsyncTask
、LiveData
、IntentService
、JobScheduler
工作或具有在背景執行緒上運作的排程器的 RxJava 管線。
由於 Flutter 是單執行緒且執行事件迴圈 (如 Node.js),因此您不必擔心執行緒管理或產生背景執行緒。如果您正在執行 I/O 繫結工作 (例如磁碟存取或網路呼叫),那麼您可以安全地使用 async
/await
,這樣就完成了。另一方面,如果您需要執行讓 CPU 忙碌的計算密集型工作,您會想要將它移至 Isolate
,以避免封鎖事件迴圈,就像您會讓任何類型的工作脫離 Android 中的主要執行緒一樣。
對於 I/O 繫結工作,請將函式宣告為 async
函式,並在函式內 await
等待長時間執行的工作
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
這通常是您執行網路或資料庫呼叫的方式,這兩者都是 I/O 操作。
在 Android 上,當您擴充 AsyncTask
時,您通常會覆寫 3 個方法:onPreExecute()
、doInBackground()
和 onPostExecute()
。Flutter 中沒有等效的方法,因為您會在長時間執行的函式上 await
等待,而 Dart 的事件迴圈會處理剩下的部分。
但是,有時候您可能會處理大量資料,導致 UI 掛起。在 Flutter 中,請使用 Isolate
來利用多個 CPU 核心來執行長時間執行或計算密集型的工作。
Isolate 是獨立的執行緒,它們不與主要執行記憶體堆積共用任何記憶體。這表示您無法從主要執行緒存取變數,或透過呼叫 setState()
來更新您的 UI。與 Android 執行緒不同,Isolate 名副其實,且無法共用記憶體 (例如以靜態欄位的形式)。
以下範例顯示,在簡單的 isolate 中,如何將資料共用回主要執行緒以更新 UI。
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first;
final msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
) as List<Object?>;
setState(() {
widgets = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future<Object?> sendReceive(SendPort port, Object? msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
在這裡,dataLoader()
是在其自己的獨立執行緒中執行的 Isolate
。在 isolate 中,您可以執行更多的 CPU 密集型處理 (例如剖析大型 JSON),或執行計算密集型數學運算,例如加密或訊號處理。
您可以在下方執行完整範例
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
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),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: getBody(),
);
}
ListView getListView() {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first;
final msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
) as List<Object?>;
setState(() {
widgets = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future<Object?> sendReceive(SendPort port, Object? msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
在 Flutter 中,OkHttp 的等價物是什麼?
#當您使用熱門的 http
套件時,在 Flutter 中進行網路呼叫很容易。
雖然 http 套件沒有 OkHttp 中的所有功能,但它會將您通常會自行實作的大部分網路連線抽象化,使其成為進行網路呼叫的簡單方式。
若要將 http
套件新增為依賴性,請執行 flutter pub add
flutter pub add http
若要進行網路呼叫,請在 async
函式 http.get()
上呼叫 await
import 'dart:developer' as developer;
import 'package:http/http.dart' as http;
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
developer.log(response.body);
}
我該如何顯示長時間執行任務的進度?
#在 Android 中,您通常會在背景執行緒上執行長時間執行的工作時,在 UI 中顯示 ProgressBar
檢視。
在 Flutter 中,請使用 ProgressIndicator
widget。透過控制它何時透過布林旗標渲染,以程式方式顯示進度。告知 Flutter 在您的長時間執行工作開始之前更新其狀態,並在工作結束後隱藏它。
在以下範例中,build 函式會分成三個不同的函式。如果 showLoadingDialog
為 true
(當 widgets.isEmpty
時),則渲染 ProgressIndicator
。否則,使用從網路呼叫傳回的資料渲染 ListView
。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
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),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: getBody(),
);
}
ListView getListView() {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
專案結構與資源
#我該將解析度相關的影像檔儲存在哪裡?
#雖然 Android 將資源和資產視為不同的項目,但 Flutter 應用程式只有資產。所有會存在於 Android 上 res/drawable-*
資料夾中的資源,都會放在 Flutter 的資產資料夾中。
Flutter 遵循簡單的基於密度的格式,如 iOS。資產可能是 1.0x
、2.0x
、3.0x
或任何其他乘數。Flutter 沒有 dp
,但有邏輯像素,這基本上與裝置獨立像素相同。Flutter 的 devicePixelRatio
表示單一邏輯像素中實體像素的比率。
相當於 Android 密度儲存區的是
Android 密度限定詞 | Flutter 像素比例 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
資產位於任何任意的資料夾中 — Flutter 沒有預先定義的資料夾結構。您在 pubspec.yaml
檔案中宣告資產 (及其位置),而 Flutter 會將它們提取出來。
儲存在原生資產資料夾中的資產會使用 Android 的 AssetManager
從原生端存取
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
Flutter 無法存取原生資源或資產。
舉例來說,若要將一個名為 my_icon.png
的新圖片素材新增到我們的 Flutter 專案,並決定將其放在一個我們任意命名為 images
的資料夾中,你會將基本圖片 (1.0x) 放在 images
資料夾中,而所有其他變體則放在以適當比例倍數命名的子資料夾中。
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
接著,你需要在你的 pubspec.yaml
檔案中宣告這些圖片。
assets:
- images/my_icon.jpeg
然後,你可以使用 AssetImage
來存取你的圖片。
AssetImage('images/my_icon.jpeg')
或直接在 Image
小部件中使用。
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
我該將字串儲存在哪裡?我該如何處理本地化?
#Flutter 目前沒有專為字串設計的資源系統。最佳且建議的做法是以鍵值對的形式將你的字串保存在 .arb
檔案中。例如:
{
"@@locale": "en",
"hello":"Hello {userName}",
"@hello":{
"description":"A message with a single parameter",
"placeholders":{
"userName":{
"type":"String",
"example":"Bob"
}
}
}
}
然後,在你的程式碼中,你可以像這樣存取你的字串:
Text(AppLocalizations.of(context)!.hello('John'));
Flutter 在 Android 上提供基本的可存取性支援,儘管此功能仍在開發中。
請參閱國際化 Flutter 應用程式以取得更多相關資訊。
Gradle 檔的等價物是什麼?我該如何新增依賴項?
#在 Android 中,你可以透過新增到 Gradle 建置腳本來新增依賴項。Flutter 使用 Dart 自己的建置系統和 Pub 套件管理器。這些工具會將原生 Android 和 iOS 包裝應用程式的建置委派給各自的建置系統。
雖然在你的 Flutter 專案的 android
資料夾下有 Gradle 檔案,但只有在你新增每個平台整合所需的原生依賴項時才使用它們。一般而言,請使用 pubspec.yaml
來宣告要在 Flutter 中使用的外部依賴項。尋找 Flutter 套件的好地方是 pub.dev。
活動 (Activities) 與片段 (fragments)
#在 Flutter 中,活動 (activities) 與片段 (fragments) 的等價物是什麼?
#在 Android 中,Activity
代表使用者可以做的單一焦點事項。Fragment
代表一種行為或使用者介面的一部分。Fragment 是一種將你的程式碼模組化、為較大螢幕構成複雜使用者介面,並協助擴展你的應用程式 UI 的方法。在 Flutter 中,這兩個概念都屬於 Widget
的範疇。
若要了解更多關於建置 Activity 和 Fragment 的 UI 的資訊,請參閱社群貢獻的 Medium 文章:給 Android 開發者的 Flutter:如何在 Flutter 中設計 Activity UI。
如「Intents」章節所述,Flutter 中的畫面由 Widget
表示,因為在 Flutter 中一切都是小部件。使用 Navigator
在代表不同畫面或頁面,或者相同資料的不同狀態或渲染的不同 Route
之間移動。
我該如何監聽 Android 活動生命週期事件?
#在 Android 中,你可以覆寫 Activity
中的方法來捕獲 activity 本身的生命週期方法,或在 Application
上註冊 ActivityLifecycleCallbacks
。在 Flutter 中,你沒有這兩種概念,但你可以透過掛鉤 WidgetsBinding
觀察器並監聽 didChangeAppLifecycleState()
變更事件來監聽生命週期事件。
可觀察的生命週期事件是:
detached
— 應用程式仍託管在 Flutter 引擎上,但與任何主機檢視分離。inactive
— 應用程式處於非使用中狀態,並且不接收使用者輸入。paused
— 應用程式目前對使用者不可見,不回應使用者輸入,並且在背景中執行。這相當於 Android 中的onPause()
。resumed
— 應用程式可見並回應使用者輸入。這相當於 Android 中的onPostResume()
。
有關這些狀態含義的更多詳細資訊,請參閱 AppLifecycleStatus
文件。
你可能已經注意到,只有一小部分 Activity 生命週期事件可用;雖然 FlutterActivity
確實會在內部捕獲幾乎所有的 activity 生命週期事件並將其傳送到 Flutter 引擎,但它們大多被你遮蔽了。Flutter 會為你處理引擎的啟動和停止,而且在大多數情況下,幾乎沒有理由需要在 Flutter 端觀察 activity 生命週期。如果你需要觀察生命週期來取得或釋放任何原生資源,你應該從原生端執行此操作。
以下是如何觀察包含 activity 的生命週期狀態的範例:
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
const LifecycleWatcher({super.key});
@override
State<LifecycleWatcher> createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher>
with WidgetsBindingObserver {
AppLifecycleState? _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null) {
return const Text(
'This widget has not observed any lifecycle changes.',
textDirection: TextDirection.ltr,
);
}
return Text(
'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr,
);
}
}
void main() {
runApp(const Center(child: LifecycleWatcher()));
}
佈局 (Layouts)
#LinearLayout 的等價物是什麼?
#在 Android 中,LinearLayout 用於線性地佈局你的小部件,可以是水平或垂直。在 Flutter 中,使用 Row 或 Column 小部件來達到相同的結果。
如果你注意到這兩個程式碼範例是相同的,除了「Row」和「Column」小部件之外。children 是相同的,並且可以利用此功能來開發可以隨著時間變化的豐富佈局,但 children 相同。
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
@override
Widget build(BuildContext context) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
若要了解更多關於建置線性佈局的資訊,請參閱社群貢獻的 Medium 文章:給 Android 開發者的 Flutter:如何在 Flutter 中設計 LinearLayout。
RelativeLayout 的等價物是什麼?
#RelativeLayout 會相對於彼此佈局你的小部件。在 Flutter 中,有幾種方法可以達到相同的結果。
你可以透過組合使用 Column、Row 和 Stack 小部件來達到 RelativeLayout 的結果。你可以指定小部件建構子關於子項相對於父項如何佈局的規則。
若要取得在 Flutter 中建置 RelativeLayout 的良好範例,請參閱 Collin 在 StackOverflow 上的回答。
ScrollView 的等價物是什麼?
#在 Android 中,使用 ScrollView 來佈局你的小部件,如果使用者的裝置的螢幕小於你的內容,它就會滾動。
在 Flutter 中,最簡單的方法是使用 ListView 小部件。從 Android 來的這看起來可能有點過頭,但在 Flutter 中,ListView 小部件同時是 ScrollView 和 Android ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
我該如何在 Flutter 中處理橫向轉換?
#如果 AndroidManifest.xml 包含,則 FlutterView 會處理設定變更
android:configChanges="orientation|screenSize"
手勢偵測與觸控事件處理
#我該如何在 Flutter 中為小部件新增點擊監聽器?
#在 Android 中,你可以透過呼叫「setOnClickListener」方法將 onClick 附加到按鈕等檢視。
在 Flutter 中,有兩種新增觸控監聽器的方法:
- 如果小部件支援事件偵測,請將一個函式傳遞給它並在該函式中處理它。例如,ElevatedButton 有一個
onPressed
參數。
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果小部件不支援事件偵測,請將小部件包裝在 GestureDetector 中,並將函式傳遞給
onTap
參數。
class SampleTapApp extends StatelessWidget {
const SampleTapApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
developer.log('tap');
},
child: const FlutterLogo(
size: 200,
),
),
),
);
}
}
我該如何處理小部件上的其他手勢?
#使用 GestureDetector,你可以監聽各種手勢,例如:
點擊
onTapDown
- 可能會導致點擊的指標已在特定位置接觸螢幕。onTapUp
- 觸發點擊的指標已停止在特定位置接觸螢幕。onTap
- 已發生點擊。onTapCancel
- 先前觸發onTapDown
的指標不會導致點擊。
雙擊
onDoubleTap
- 使用者在同一位置快速連續點擊螢幕兩次。
長按
onLongPress
- 指標在同一位置與螢幕保持接觸很長一段時間。
垂直拖曳
onVerticalDragStart
- 指標已接觸螢幕,並且可能會開始垂直移動。onVerticalDragUpdate
- 與螢幕接觸的指標已在垂直方向上移動更遠。onVerticalDragEnd
- 先前與螢幕接觸並垂直移動的指標不再與螢幕接觸,並且在停止接觸螢幕時以特定的速度移動。
水平拖曳
onHorizontalDragStart
- 指標已接觸螢幕,並且可能會開始水平移動。onHorizontalDragUpdate
- 與螢幕接觸的指標已在水平方向上移動更遠。onHorizontalDragEnd
- 先前與螢幕接觸並水平移動的指標不再與螢幕接觸,並且在停止接觸螢幕時以特定的速度移動。
以下範例顯示一個 GestureDetector
,它會在雙擊時旋轉 Flutter 標誌:
class SampleApp extends StatefulWidget {
const SampleApp({super.key});
@override
State<SampleApp> createState() => _SampleAppState();
}
class _SampleAppState extends State<SampleApp>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
child: RotationTransition(
turns: curve,
child: const FlutterLogo(
size: 200,
),
),
),
),
);
}
}
列表視圖 (Listviews) 與適配器 (adapters)
#在 Flutter 中,ListView 的替代方案是什麼?
#Flutter 中 ListView 的等效項是...一個 ListView!
在 Android ListView 中,你會建立一個 adapter 並將其傳遞到 ListView,它會使用你的 adapter 回傳的內容來渲染每一列。但是,你必須確保回收你的列,否則你會遇到各種瘋狂的視覺故障和記憶體問題。
由於 Flutter 的不可變小部件模式,你會將小部件清單傳遞到你的 ListView,而 Flutter 會負責確保滾動快速且順暢。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
));
}
return widgets;
}
}
我該如何知道點擊了哪個列表項目?
#在 Android 中,ListView 有一個方法可以找出點擊了哪個項目,即「onItemClickListener」。在 Flutter 中,請使用傳入的小部件提供的觸控處理。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
}
我該如何動態更新 ListView?
#在 Android 上,你會更新 adapter 並呼叫 notifyDataSetChanged
。
在 Flutter 中,如果你要更新 setState()
內的 widget 清單,你會很快發現你的資料在視覺上沒有改變。這是因為當呼叫 setState()
時,Flutter 渲染引擎會檢查小部件樹以查看是否有任何變更。當它到達你的 ListView
時,它會執行 ==
檢查,並確定這兩個 ListView
是相同的。沒有任何變更,因此不需要更新。
若要以簡單的方式更新你的 ListView
,請在 setState()
內建立一個新的 List
,並將資料從舊清單複製到新清單。雖然此方法很簡單,但不建議用於大型資料集,如下一個範例所示。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
);
}
}
建議的、有效率且有效的方式是使用 ListView.Builder
來建置清單。當你有動態 List
或具有大量資料的 List
時,此方法非常有用。這本質上相當於 Android 上的 RecyclerView,它會自動為你回收清單元素。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
);
}
}
不要建立「ListView」,而是建立一個 ListView.builder
,它會接受兩個關鍵參數:清單的初始長度,以及一個 ItemBuilder
函式。
ItemBuilder
函式類似於 Android adapter 中的 getView
函式;它會採用位置,並回傳你想要在該位置渲染的列。
最後但最重要的是,請注意 onTap()
函式不再重新建立列表,而是改為使用 .add
方法將項目加入列表中。
文字處理
#我該如何在我的 Text 小部件上設定自定義字型?
#在 Android SDK 中(截至 Android O),您會建立一個字型資源檔案,並將其傳遞給 TextView 的 FontFamily 參數。
在 Flutter 中,將字型檔案放在一個資料夾中,並在 pubspec.yaml
檔案中參考它,這與您匯入圖片的方式類似。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然後將字型指定給您的 Text
小部件。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: const Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
我該如何設定我的 Text 小部件的樣式?
#除了字型之外,您還可以自訂 Text
小部件上的其他樣式元素。Text
小部件的 style 參數接受一個 TextStyle
物件,您可以在其中自訂許多參數,例如:
- color(顏色)
- decoration(裝飾)
- decorationColor(裝飾顏色)
- decorationStyle(裝飾樣式)
- fontFamily(字型系列)
- fontSize(字型大小)
- fontStyle(字型樣式)
- fontWeight(字型粗細)
- hashCode
- height(行高)
- inherit(繼承)
- letterSpacing(字母間距)
- textBaseline(文字基準線)
- wordSpacing(單字間距)
表單輸入
#如需更多關於使用表單的資訊,請參閱 Retrieve the value of a text field,來自 Flutter 食譜。
在輸入欄位 (Input) 上,「提示 (hint)」的等價物是什麼?
#在 Flutter 中,您可以透過將 InputDecoration 物件新增到 Text 小部件的 decoration 建構子參數中,輕鬆地為您的輸入顯示「提示」或預留位置文字。
Center(
child: TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
),
)
我該如何顯示驗證錯誤?
#就像使用「提示」一樣,將一個 InputDecoration 物件傳遞給 Text 小部件的 decoration 建構子。
但是,您不希望一開始就顯示錯誤。相反地,當使用者輸入了無效的資料時,請更新狀態,並傳遞一個新的 InputDecoration
物件。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String? _errorText;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: TextField(
onSubmitted: (text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(
hintText: 'This is a hint',
errorText: _getErrorText(),
),
),
),
);
}
String? _getErrorText() {
return _errorText;
}
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
}
Flutter 外掛程式
#我該如何存取 GPS 感測器?
#使用 geolocator
社群外掛程式。
我該如何存取相機?
#image_picker
外掛程式廣泛用於存取相機。
我該如何使用 Facebook 登入?
#若要使用 Facebook 登入,請使用 flutter_facebook_login
社群外掛程式。
我該如何使用 Firebase 功能?
#大多數 Firebase 功能都由 第一方外掛程式涵蓋。這些外掛程式是第一方整合,由 Flutter 團隊維護。
google_mobile_ads
用於 Flutter 的 Google 行動廣告firebase_analytics
用於 Firebase Analyticsfirebase_auth
用於 Firebase Authfirebase_database
用於 Firebase RTDBfirebase_storage
用於 Firebase Cloud Storagefirebase_messaging
用於 Firebase Messaging (FCM)flutter_firebase_ui
用於快速整合 Firebase Auth(Facebook、Google、Twitter 和電子郵件)cloud_firestore
用於 Firebase Cloud Firestore
您也可以在 pub.dev 上找到一些第三方 Firebase 外掛程式,這些外掛程式涵蓋第一方外掛程式未直接涵蓋的領域。
我該如何建立我自己的自定義原生整合?
#如果 Flutter 或其社群外掛程式缺少特定平台的功能,您可以依照開發套件和外掛程式頁面的說明,建立自己的外掛程式。
簡而言之,Flutter 的外掛程式架構非常像在 Android 中使用事件匯流排:您發出一個訊息,並讓接收者處理並將結果傳回給您。在此情況下,接收者是在 Android 或 iOS 的原生端執行的程式碼。
我該如何在我的 Flutter 應用程式中使用 NDK?
#如果您在目前的 Android 應用程式中使用 NDK,並且希望您的 Flutter 應用程式能夠利用您的原生程式庫,那麼可以透過建立自訂外掛程式來實現。
您的自訂外掛程式首先會與您的 Android 應用程式交談,在其中您會透過 JNI 呼叫您的 native
函式。一旦回應準備就緒,請將訊息傳回 Flutter 並呈現結果。
目前不支援從 Flutter 直接呼叫原生程式碼。
主題
#我該如何為我的應用程式設定主題?
#Flutter 開箱即用,提供了 Material Design 的優美實作,它可以處理您通常需要執行的許多樣式和主題需求。與 Android 在 XML 中宣告主題,然後使用 AndroidManifest.xml 將其指定給您的應用程式不同,在 Flutter 中,您是在最上層小部件中宣告主題。
若要充分利用應用程式中的 Material Components,您可以宣告最上層小部件 MaterialApp
作為應用程式的進入點。MaterialApp 是一個方便的小部件,它包裝了許多實作 Material Design 的應用程式通常需要的小部件。它透過新增 Material 特有的功能建立在 WidgetsApp 之上。
您也可以使用 WidgetsApp
作為您的應用程式小部件,它提供一些相同的功能,但不如 MaterialApp
豐富。
若要自訂任何子元件的色彩和樣式,請將 ThemeData
物件傳遞給 MaterialApp
小部件。例如,在下面的程式碼中,種子的色彩配置設定為 deepPurple,文字選取顏色設定為紅色。
import 'package:flutter/material.dart';
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(),
);
}
}
資料庫與本地儲存
#我該如何存取 Shared Preferences?
#在 Android 中,您可以使用 SharedPreferences API 儲存一小組鍵值對。
在 Flutter 中,使用 Shared_Preferences 外掛程式存取此功能。此外掛程式包裝了 Shared Preferences 和 NSUserDefaults (iOS 對應項) 的功能。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
Future<void> _incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
await prefs.setInt('counter', counter);
}
我該如何在 Flutter 中存取 SQLite?
#在 Android 中,您可以使用 SQLite 來儲存可以使用 SQL 查詢的結構化資料。
在 Flutter 中,對於 macOS、Android 或 iOS,請使用 SQFlite 外掛程式存取此功能。
偵錯
#我可以使用哪些工具來偵錯我的 Flutter 應用程式?
#使用 DevTools 套件來偵錯 Flutter 或 Dart 應用程式。
DevTools 包含對效能分析、檢查堆積、檢視小部件樹狀結構、記錄診斷、偵錯、觀察已執行的程式碼行、偵錯記憶體洩漏和記憶體碎片的支持。如需更多資訊,請參閱 DevTools 文件。
通知
#我該如何設定推送通知?
#在 Android 中,您可以使用 Firebase Cloud Messaging 為您的應用程式設定推播通知。
在 Flutter 中,請使用 Firebase Messaging 外掛程式存取此功能。如需更多關於使用 Firebase Cloud Messaging API 的資訊,請參閱 firebase_messaging
外掛程式文件。
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-10-17。 檢視原始碼或回報問題。