Flutter 給 Android 開發者的指南
學習如何將 Android 開發經驗應用於構建 Flutter 應用。
本文件旨在幫助 Android 開發者利用現有的 Android 知識,透過 Flutter 構建移動應用。如果你瞭解 Android 框架的基礎知識,可以將本文件作為 Flutter 開發的入門指南。
你的 Android 知識和技能在構建 Flutter 應用時非常有價值,因為 Flutter 依賴移動作業系統提供的眾多功能和配置。Flutter 是一種構建移動 UI 的新方式,但它擁有一套外掛系統來與 Android (及 iOS) 進行非 UI 任務的通訊。如果你是 Android 專家,你不需要重新學習一切即可使用 Flutter。
本文件可以像菜譜一樣使用,您可以隨意跳轉並查詢與您的需求最相關的問題。
檢視
#Flutter 中是否有 View 的等價物?
#在 Android 中,View 是螢幕上顯示的一切事物的基石。按鈕、工具欄和輸入框,一切皆為 View。在 Flutter 中,View 的大致等價物是 Widget。Widget 與 Android 的檢視並非一一對應,但在你熟悉 Flutter 的過程中,你可以將它們理解為“你宣告和構建 UI 的方式”。
然而,它們與 View 存在幾點不同。首先,Widget 的生命週期不同:它們是不可變的,僅在需要改變時才存在。每當 Widget 或其狀態發生變化時,Flutter 框架會建立一個新的 Widget 例項樹。相比之下,Android 檢視繪製一次後,除非呼叫 invalidate,否則不會重繪。
Flutter 的 Widget 是輕量級的,這部分歸功於它們的不可變性。因為它們本身不是檢視,也不直接繪製任何東西,而是對 UI 及其語義的描述,這些描述在後臺被“充氣”(inflated) 成實際的檢視物件。
Flutter 包含了 Material Components 庫。這些 Widget 實現了 Material Design 指南。Material Design 是一套靈活的設計系統,針對所有平臺進行了最佳化,包括 iOS。
但 Flutter 足夠靈活和具有表現力,可以實現任何設計語言。例如,在 iOS 上,你可以使用 Cupertino Widget 來生成看起來像 Apple iOS 設計語言 的介面。
如何更新 Widget?
#在 Android 中,你透過直接修改檢視來更新它們。然而,在 Flutter 中,Widget 是不可變的,不能直接更新,你必須操作 Widget 的狀態。
這就是 Stateful (有狀態) 和 Stateless (無狀態) Widget 概念的由來。StatelessWidget 正如其名——一個沒有狀態資訊的 Widget。
當描述的 UI 部分除了物件中的配置資訊外不依賴其他內容時,StatelessWidget 非常有用。
例如,在 Android 中,這類似於放置一個帶有 logo 的 ImageView。logo 在執行時不會改變,所以使用 Flutter 的 StatelessWidget。
如果你想根據 HTTP 請求收到的資料或使用者互動來動態改變 UI,那麼你必須使用 StatefulWidget,並告知 Flutter 框架該 Widget 的 State 已更新,以便它能更新該 Widget。
這裡需要注意的是,本質上無狀態和有狀態 Widget 的表現是一樣的。它們在每一幀都會重建,不同之處在於 StatefulWidget 擁有一個 State 物件,該物件可以跨幀儲存狀態資料並在重建時恢復它。
如果有疑問,請記住這條規則:如果一個 Widget 發生變化(例如由於使用者互動),那麼它就是有狀態的。但是,如果一個 Widget 只是響應變化,那麼只要包含它的父 Widget 本身不響應變化,它仍然可以是無狀態的。
以下示例展示瞭如何使用 StatelessWidget。一個常見的 StatelessWidget 是 Text 元件。如果你檢視 Text 元件的實現,你會發現它繼承自 StatelessWidget。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
正如你所見,Text Widget 沒有與之關聯的狀態資訊,它只是渲染其建構函式中傳遞的內容,僅此而已。
但是,如果你想讓“I Like 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),
),
);
}
}
如何佈局 Widget?XML 佈局檔案在哪裡?
#在 Android 中,你使用 XML 編寫佈局;而在 Flutter 中,你使用 Widget 樹編寫佈局。
以下示例展示瞭如何顯示一個帶填充的簡單 Widget
@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'),
),
),
);
}
你可以在 Widget 目錄 中檢視 Flutter 提供的一些佈局。
如何從佈局中新增或刪除元件?
#在 Android 中,你在父元件上呼叫 addChild() 或 removeChild() 來動態新增或刪除子檢視。在 Flutter 中,由於 Widget 是不可變的,沒有 addChild() 的直接等價物。相反,你可以將一個返回 Widget 的函式傳遞給父元件,並使用布林標誌來控制該子元件的建立。
例如,這裡展示瞭如何在點選 FloatingActionButton 時在兩個 Widget 之間切換:
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),
),
);
}
}
如何讓 Widget 產生動畫效果?
#在 Android 中,你可以透過 XML 或呼叫檢視的 animate() 方法來建立動畫。在 Flutter 中,透過將 Widget 包裝在動畫 Widget 中來使用動畫庫對 Widget 進行動畫處理。
在 Flutter 中,使用 AnimationController,它是一個 Animation,可以暫停、查詢、停止和反轉動畫。它需要一個 Ticker,用於在 vsync 發生時發出訊號,並在執行時在每一幀產生 0 到 1 之間的線性插值。然後,你建立一個或多個 Animation 並將它們附加到控制器上。
例如,您可以使用 `CurvedAnimation` 來實現沿著插值曲線的動畫。從這個意義上說,控制器是動畫進度的“主”源,而 `CurvedAnimation` 計算替換控制器預設線性運動的曲線。像元件一樣,Flutter 中的動畫也透過組合工作。
在構建 Widget 樹時,將 Animation 分配給 Widget 的動畫屬性(例如 FadeTransition 的不透明度),並告訴控制器開始動畫。
以下示例展示瞭如何編寫一個 FadeTransition,當您按下 FloatingActionButton 時,該轉換會將 Widget 淡入為徽標
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),
),
);
}
}
有關更多資訊,請參閱動畫與動作 Widgets、動畫教程和動畫概述。
如何使用 Canvas 進行繪製?
#在 Android 中,你會使用 Canvas 和 Drawable 在螢幕上繪製圖像和形狀。Flutter 也擁有類似的 Canvas API,因為它基於相同的底層渲染引擎 Skia。因此,對於 Android 開發者來說,在 Flutter 中進行 Canvas 繪製是一項非常熟悉的任務。
Flutter 有兩個類可以幫助你在 Canvas 上繪製:CustomPaint 和 CustomPainter,後者實現了你在 Canvas 上繪製的演算法。
要了解如何在 Flutter 中實現簽名繪製器,請檢視 Collin 在 StackOverflow 上關於 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;
}
如何構建自定義 Widget?
#在 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));
}
}
然後像使用任何其他 Flutter Widget 一樣使用 CustomButton
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}
Intent
#Flutter 中是否有 Intent 的等價物?
#在 Android 中,Intent 有兩個主要用途:在 Activity 之間導航,以及與元件通訊。另一方面,Flutter 沒有 Intent 的概念,儘管你仍然可以透過原生整合(使用外掛)啟動 Intent。
Flutter 實際上沒有 Activity 和 Fragment 的直接等價物;相反,在 Flutter 中,你使用 Navigator 和 Route 在同一 Activity 內的不同螢幕之間導航。
Route 是應用“螢幕”或“頁面”的抽象,而 Navigator 是管理路由的 Widget。路由大致對映到 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 中如何處理來自外部應用的 Intent?
#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,提取其中共享的文字並將其保留。當 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 as String;
});
}
}
}
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 或 Kotlin 的協程,你可能對 async/await 正規化很熟悉。
例如,您可以使用 async/await 執行網路程式碼,而不會導致 UI 卡頓,讓 Dart 完成繁重的工作
Future<void> loadData() async {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
一旦 awaited 的網路呼叫完成,透過呼叫 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<Map<String, Object?>> 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 {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
}
有關後臺處理的更多資訊,以及 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 {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
這是你通常進行網路或資料庫呼叫的方式,兩者都是 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 as SendPort;
final msg =
await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
)
as List<Object?>;
final posts = msg.cast<Map<String, Object?>>();
setState(() {
widgets = posts;
});
}
// 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 dataUrl = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
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<Map<String, Object?>> 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 as SendPort;
final msg =
await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
)
as List<Object?>;
final posts = msg.cast<Map<String, Object?>>();
setState(() {
widgets = posts;
});
}
// 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 dataUrl = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
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
要進行網路呼叫,在非同步函式 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 更新其狀態,並在任務結束後將其隱藏。
在以下示例中,構建函式被拆分為三個不同的函式。如果 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<Map<String, Object?>> 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 {
final dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(dataURL);
setState(() {
widgets = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>();
});
}
}
專案結構與資源
#在哪裡存放與解析度相關的圖片檔案?
#雖然 Android 將資源和資產視為不同的項,但 Flutter 應用只有資產 (Assets)。所有在 Android 上存放在 res/drawable-* 資料夾中的資源,在 Flutter 中都存放在 assets 資料夾中。
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.png
然後你可以使用 AssetImage 訪問你的影像:
AssetImage('images/my_icon.png'),
或者直接在 Image Widget 中使用:
@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 應用。
Gradle 檔案的等價物是什麼?如何新增依賴?
#在 Android 中,你透過新增到 Gradle 構建指令碼來新增依賴。Flutter 使用 Dart 自己的構建系統和 Pub 包管理器。這些工具會將原生 Android 和 iOS 包裝應用的構建委託給各自的構建系統。
雖然你的 Flutter 專案的 android 資料夾下有 Gradle 檔案,但只有在新增每個平臺整合所需的原生依賴時才使用它們。通常,使用 pubspec.yaml 來宣告 Flutter 中使用的外部依賴。查詢 Flutter 包的一個好地方是 pub.dev。
Activity 和 Fragment
#Flutter 中是否有 Activity 和 Fragment 的等價物?
#在 Android 中,Activity 代表使用者可以執行的單一聚焦任務。Fragment 代表行為或使用者介面的一部分。Fragment 是模組化程式碼、為大屏組合複雜介面以及幫助擴充套件應用 UI 的一種方式。在 Flutter 中,這兩個概念都屬於 Widget 的範疇。
要了解更多關於構建 Activity 和 Fragment UI 的內容,請參閱社群貢獻的文章:Flutter 給 Android 開發者的指南:如何在 Flutter 中設計 Activity UI。
正如在Intent部分提到的,Flutter 中的螢幕由 Widget 表示,因為 Flutter 中一切皆為 Widget。使用 Navigator 在代表不同螢幕或頁面的不同 Route 之間移動,或者表示同一資料的不同狀態或渲染。
如何監聽 Android Activity 生命週期事件?
#在 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()));
}
佈局
#LinearLayout 的等價物是什麼?
#在 Android 中,LinearLayout 用於水平或垂直線性佈局 Widget。在 Flutter 中,使用 Row 或 Column Widget 來實現相同的結果。
你會發現這兩個程式碼示例除了“Row”和“Column”Widget 外完全相同。子 Widget 是相同的,這一特性可以用來開發可以隨時間變化的豐富佈局。
@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 文章:Flutter 給 Android 開發者的指南:如何在 Flutter 中設計 LinearLayout。
RelativeLayout 的等價物是什麼?
#RelativeLayout 將 Widget 相對於彼此進行佈局。在 Flutter 中,有幾種方法可以實現相同的結果。
你可以透過結合使用 Column、Row 和 Stack Widget 來實現 RelativeLayout 的結果。你可以在 Widget 建構函式中指定有關子項如何相對於父項進行佈局的規則。
要檢視在 Flutter 中構建 RelativeLayout 的好例子,請參見 Collin 在 StackOverflow 上的回答。
ScrollView 的等價物是什麼?
#在 Android 中,使用 ScrollView 來佈局 Widget——如果使用者裝置的螢幕比你的內容小,它會滾動。
在 Flutter 中,最簡單的方法是使用 ListView Widget。對於來自 Android 的你來說,這可能看起來有些過度設計,但在 Flutter 中,ListView Widget 同時兼具 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 中為 Widget 新增點選監聽器?
#在 Android 中,你可以透過呼叫方法 'setOnClickListener' 將點選事件附加到按鈕等檢視上。
在 Flutter 中,有兩種新增觸控監聽器的方法:
- 如果 Widget 支援事件檢測,則將函式傳遞給它並在函式中處理。例如,
ElevatedButton有一個onPressed引數。
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果 Widget 不支援事件檢測,請將該 Widget 包裝在
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),
),
),
);
}
}
如何處理 Widget 上的其他手勢?
#使用 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),
),
),
),
);
}
}
ListView 與 Adapter
#Flutter 中 ListView 的替代品是什麼?
#Flutter 中 ListView 的等價物是……ListView!
在 Android ListView 中,你建立一個 Adapter 並將其傳入 ListView,後者會渲染 Adapter 返回的每一行。但是,你必須確保回收行,否則會遇到各種視覺故障和記憶體問題。
由於 Flutter 的不可變 Widget 模式,你將 Widget 列表傳給 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 中,使用傳入 Widget 提供的觸控處理。
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 渲染引擎會檢查 Widget 樹看是否有變化。當它到達你的 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 Widget
@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 Widget 上自定義其他樣式元素。Text Widget 的 style 引數接受一個 TextStyle 物件,您可以在其中自定義許多引數,例如
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
表單輸入
#有關使用表單的更多資訊,請參閱 獲取文字欄位的值。
輸入框中 "hint" 的等價物是什麼?
#在 Flutter 中,透過將 InputDecoration 物件新增到 Text Widget 的 decoration 建構函式引數,可以輕鬆地為你的輸入顯示“hint”或佔位符文字。
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)
如何顯示驗證錯誤?
#就像使用“hint”一樣,將 InputDecoration 物件傳遞給 Text Widget 的 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 分析。 firebase_auth:用於 Firebase 認證。-
firebase_database:用於 Firebase 即時資料庫。 -
firebase_storage:用於 Firebase 雲端儲存。 -
firebase_messaging:用於 Firebase 訊息通知 (FCM)。 -
flutter_firebase_ui:用於快速 Firebase 認證整合(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 不同(在 Android 中你在 XML 中宣告主題,然後使用 AndroidManifest.xml 將其分配給應用),在 Flutter 中,你在頂層 Widget 中宣告主題。
要充分利用應用中的 Material Components,你可以宣告一個 MaterialApp 頂層 Widget 作為應用的入口點。MaterialApp 是一個方便的 Widget,它包裝了實現 Material Design 的應用通常所需的許多 Widget。它在 WidgetsApp 的基礎上添加了 Material 特定的功能。
您還可以使用 WidgetsApp 作為您的應用 Widget,它提供了一些相同的功能,但不如 MaterialApp 豐富。
要自定義任何子元件的顏色和樣式,請將 ThemeData 物件傳遞給 MaterialApp Widget。例如,在下面的程式碼中,配色方案的種子設定為 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(),
);
}
}
桌面小部件 (Homescreen widgets)
#如何建立桌面小部件?
#Android 桌面小部件不能完全使用 Flutter 建立。它們必須使用 Jetpack Glance(首選方法)或 XML 佈局程式碼。使用第三方包 home_widget,你可以將桌面小部件連線到 Dart 程式碼,在宿主小部件中嵌入 Flutter 元件(作為影像),並在 Flutter 與桌面小部件之間共享資料。
為了提供更豐富、更具吸引力的體驗,建議新增小部件預覽以包含在小部件選擇器中。對於執行 Android 15 及更高版本的裝置,生成的預覽允許使用者看到目標小部件的動態和個性化版本,讓他們一睹它在主螢幕上的確切效果。有關生成的預覽和舊裝置的回退選項的更多資訊,請檢視在小部件選擇器中新增生成的預覽文件頁面。
資料庫與本地儲存
#如何訪問 SharedPreferences?
#在 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 支援效能分析、檢查堆、檢查 widget 樹、日誌診斷、除錯、觀察已執行程式碼行、除錯記憶體洩漏和記憶體碎片。有關更多資訊,請檢視 DevTools 文件。
通知
#如何設定推送通知?
#在 Android 中,你使用 Firebase Cloud Messaging 為你的應用設定推送通知。
在 Flutter 中,使用 Firebase Messaging 外掛訪問此功能。有關使用 Firebase Cloud Messaging API 的更多資訊,請參閱 firebase_messaging 外掛文件。