Flutter for Android developers
瞭解如何將 Android 開發人員知識應用於構建 Flutter 應用程式。
本文件面向希望將現有的 Android 知識應用於使用 Flutter 構建移動應用程式的 Android 開發人員。如果您瞭解 Android 框架的基礎知識,則可以使用本文件作為 Flutter 開發的起點。
您的 Android 知識和技能在構建 Flutter 時非常有價值,因為 Flutter 依賴於移動作業系統來實現許多功能和配置。Flutter 是一種構建移動 UI 的新方法,但它具有一個外掛系統,可以與 Android(和 iOS)進行通訊以執行非 UI 任務。如果您是 Android 專家,則無需重新學習所有內容即可使用 Flutter。
本文件可以像菜譜一樣使用,您可以隨意跳轉並查詢與您的需求最相關的問題。
檢視
#Flutter 中 View 的等效項是什麼?
#在 Android 中,View 是顯示在螢幕上的所有內容的基礎。按鈕、工具欄和輸入,一切都是 View。在 Flutter 中,View 的粗略等效項是 Widget。Widgets 並不能完全對映到 Android views,但在您熟悉 Flutter 的工作方式時,您可以將它們視為“宣告和構建 UI 的方式”。
但是,這些與 View 有一些區別。首先,widgets 具有不同的生命週期:它們是不可變的,並且僅在需要更改時才存在。每當 widgets 或其狀態發生更改時,Flutter 框架都會建立一個新的 widget 例項樹。相比之下,Android view 繪製一次,除非呼叫 invalidate,否則不會重新繪製。
Flutter 的 widgets 是輕量級的,部分原因在於它們的不可變性。由於它們本身不是 views,並且沒有直接繪製任何內容,而是 UI 和語義的描述,這些描述在底層被“膨脹”成實際的 view 物件。
Flutter 包含 Material Components 庫。這些是實現 Material Design 指南 的 widgets。Material Design 是一種靈活的設計系統 針對所有平臺進行了最佳化,包括 iOS。
但是 Flutter 靈活且富有表現力,足以實現任何設計語言。例如,在 iOS 上,您可以使用 Cupertino widgets 來生成看起來像 Apple 的 iOS 設計語言 的介面。
如何更新 widgets?
#在 Android 中,您透過直接修改 views 來更新它們。但是,在 Flutter 中,Widgets 是不可變的,並且不能直接更新,而是必須使用 widget 的狀態。
這就是 Stateful 和 Stateless widgets 的概念由來。StatelessWidget 就是字面意思——一個沒有狀態資訊的 widget。
StatelessWidgets 在您描述的使用者介面部分不依賴於物件中的配置資訊以外的任何內容時很有用。
例如,在 Android 中,這類似於放置一個帶有您徽標的 ImageView。徽標在執行時不會更改,因此在 Flutter 中使用 StatelessWidget。
如果您想根據在進行 HTTP 呼叫或使用者互動後接收到的資料動態更改 UI,那麼必須使用 StatefulWidget 並告訴 Flutter 框架 widget 的 State 已更新,以便它可以更新該 widget。
重要的是要注意,無論是有狀態還是無狀態的 widgets,其核心行為都相同。它們每幀都會重建,區別在於 StatefulWidget 具有一個 State 物件,該物件跨幀儲存狀態資料並恢復它。
如果您有疑問,請始終記住此規則:如果 widget 發生更改(例如,由於使用者互動),則它是 stateful 的。但是,如果 widget 對更改做出反應,則包含的父 widget 仍然可以是 stateless 的,如果它本身不響應更改。
以下示例顯示瞭如何使用 StatelessWidget。一個常見的 StatelessWidget 是 Text widget。如果您檢視 Text widget 的實現,您會發現它繼承自 StatelessWidget。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如您所見,Text Widget 沒有與之關聯的狀態資訊,它僅呈現其建構函式中傳遞的內容,僅此而已。
但是,如果您想讓“I Like Flutter” 動態更改,例如,單擊 FloatingActionButton 時會發生什麼?
為此,將 Text widget 包裝在 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),
),
);
}
}
如何佈局我的 widgets?我的 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 catalog 中檢視 Flutter 提供的某些佈局。
如何新增或刪除佈局中的元件?
#在 Android 中,您透過在父級上呼叫 addChild() 或 removeChild() 來動態新增或刪除子 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),
),
);
}
}
如何為 widget 新增動畫?
#在 Android 中,您可以使用 XML 建立動畫,或者呼叫 view 上的 animate() 方法。在 Flutter 中,透過將 widgets 包裝在動畫 widget 中來為 widgets 新增動畫。
在 Flutter 中,使用 AnimationController,它是一個 Animation<double>,可以暫停、查詢、停止和反轉動畫。它需要一個 Ticker,該 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。因此,在 Flutter 中繪製到 canvas 對於 Android 開發人員來說是一項非常熟悉的任務。
Flutter 有兩個類可以幫助您繪製到 canvas:CustomPaint 和 CustomPainter,後者實現了您的演算法來繪製到 canvas。
要了解如何在 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;
}
如何構建自定義 widgets?
#在 Android 中,您通常透過繼承 View 或使用預先存在的 view 來覆蓋並實現實現所需行為的方法。
在 Flutter 中,透過 組合 較小的 widgets(而不是擴充套件它們)來構建自定義 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'));
}
Intents
#Flutter 中 Intent 的等效項是什麼?
#在 Android 中,Intent 有兩個主要用例:在 Activities 之間導航以及與元件通訊。另一方面,Flutter 沒有 intents 的概念,儘管您仍然可以透過本機整合(使用 外掛)啟動 intents。
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 中處理來自外部應用程式的傳入 intents?
#Flutter 可以透過直接與 Android 層通訊並請求共享的資料來處理來自 Android 的傳入 Intent。
以下示例在原生 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 as String;
});
}
}
}
startActivityForResult() 的等效項是什麼?
#Navigator 類處理 Flutter 中的路由,並用於從您推送到堆疊上的路由中獲取結果。這是透過 await 等待 push() 返回的 Future 來完成的。
例如,要啟動一個允許使用者選擇位置的 Location 路由,您可以執行以下操作
Object? coordinates = await Navigator.of(context).pushNamed('/location');
然後,在您的 Location 路由內部,一旦使用者選擇了他們的位置,您可以 pop 堆疊並返回結果
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
Async 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?>>();
});
}
一旦 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<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
要進行網路呼叫,請對 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<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 應用程式只有資產。所有將位於 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.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。
Activities 和 fragments
#Flutter 中 activities 和 fragments 的等效項是什麼?
#在 Android 中,Activity 表示使用者可以執行的單個專注活動。Fragment 表示行為或使用者介面的部分。Fragment 是一種模組化程式碼、組合用於較大螢幕的複雜使用者介面以及幫助擴充套件應用程式 UI 的方法。在 Flutter 中,這兩個概念都包含在 Widget 中。
要了解有關構建 Activity 和 Fragment 的 UI 的更多資訊,請參閱社群貢獻的文章:Flutter for Android Developers: How to design Activity UI in Flutter。
如 Intents 部分所述,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 之外是相同的。子項相同,並且可以利用此功能開發可以隨時間變化且具有相同子項的豐富佈局。
@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 for Android Developers: How to design LinearLayout in Flutter。
RelativeLayout 的等效項是什麼?
#RelativeLayout 按照彼此之間的關係佈局您的 Widget。在 Flutter 中,有幾種方法可以實現相同的結果。
您可以使用 Column、Row 和 Stack Widget 的組合來實現 RelativeLayout 的效果。您可以在 Widget 建構函式中指定規則,以確定子項相對於父項的佈局方式。
有關在 Flutter 中構建 RelativeLayout 的良好示例,請參閱 Collin 在 StackOverflow 上的回答。
ScrollView 的等效項是什麼?
#在 Android 中,使用 ScrollView 佈局您的 Widget——如果使用者的裝置螢幕小於您的內容,則會滾動。
在 Flutter 中,最簡單的方法是使用 ListView Widget。這可能看起來有點過頭了,但 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 中處理橫向轉換?
#FlutterView 會處理配置更改,如果 AndroidManifest.xml 包含
android:configChanges="orientation|screenSize"
手勢檢測和觸控事件處理
#如何在 Flutter 中為 widget 新增 onClick 監聽器?
#在 Android 中,您可以透過呼叫 'setOnClickListener' 方法將 onClick 附加到諸如 button 這樣的檢視。
在 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),
),
),
);
}
}
如何處理 widgets 上的其他手勢?
#使用 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 中,您建立一個介面卡並將其傳遞到 ListView,ListView 使用介面卡返回的內容渲染每一行。但是,您必須確保回收您的行,否則您會遇到各種視覺故障和記憶體問題。
由於 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 上,您更新介面卡並呼叫 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 介面卡中的 getView 函式;它接受一個位置,並返回要在該位置渲染的行。
最後,但最重要的是,請注意 onTap() 函式不再重新建立列表,而是 .add 到列表中。
處理文字
#如何在我的 Text widgets 上設定自定義字型?
#在 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 widgets 的樣式?
#除了字型之外,您還可以在 Text Widget 上自定義其他樣式元素。Text Widget 的 style 引數接受一個 TextStyle 物件,您可以在其中自定義許多引數,例如
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
表單輸入
#有關使用表單的更多資訊,請參閱 Retrieve the value of a text field。
Input 上的“提示”的等效項是什麼?
#在 Flutter 中,您可以透過將 InputDecoration 物件新增到 Text Widget 的 decoration 建構函式引數中,輕鬆顯示輸入框的“提示”或佔位符文字。
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a 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 Mobile Ads -
firebase_analytics用於 Firebase Analytics firebase_auth用於 Firebase Auth-
firebase_database用於 Firebase RTDB -
firebase_storage用於 Firebase Cloud Storage -
firebase_messaging用於 Firebase Messaging (FCM) -
flutter_firebase_ui用於快速的 Firebase Auth 整合(Facebook、Google、Twitter 和電子郵件) -
cloud_firestore用於 Firebase Cloud Firestore
您還可以找到一些第三方 Firebase 外掛在 pub.dev 上,涵蓋第一方外掛未直接涵蓋的領域。
如何構建我自己的自定義本機整合?
#如果存在 Flutter 或其社群外掛中缺少平臺特定功能,您可以按照 開發包和外掛 頁面構建自己的外掛。
總而言之,Flutter 的外掛架構類似於在 Android 中使用事件匯流排:您傳送一條訊息,讓接收者處理並向您發出結果。在這種情況下,接收者是在 Android 或 iOS 上執行的本機程式碼。
如何在我的 Flutter 應用程式中使用 NDK?
#如果您在當前的 Android 應用程式中使用 NDK,並希望您的 Flutter 應用程式利用您的本機庫,那麼可以透過構建自定義外掛來實現。
您的自定義外掛首先與您的 Android 應用程式通訊,您在其中透過 JNI 呼叫您的 native 函式。一旦準備好響應,就將訊息傳送回 Flutter 並渲染結果。
目前不支援直接從 Flutter 呼叫本機程式碼。
主題
#如何設定我的應用程式的主題?
#開箱即用,Flutter 附帶了 Material Design 的精美實現,它處理了您通常需要執行的許多樣式和主題需求。與 Android 不同,您在 XML 中宣告主題並在 AndroidManifest.xml 中使用它將主題分配給您的應用程式,在 Flutter 中,您在頂級 Widget 中宣告主題。
為了充分利用應用程式中的 Material 元件,您可以宣告一個頂級 Widget MaterialApp 作為應用程式的入口點。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
#如何建立一個 homescreen widget?
#使用 Flutter 無法完全建立 Android 主螢幕小部件。 必須使用 Jetpack Glance(首選方法)或 XML 佈局程式碼。 透過第三方軟體包 home_widget,您可以將主螢幕小部件與 Dart 程式碼連線起來,將 Flutter 元件(作為影像)嵌入到宿主小部件中,並在 Flutter 與主螢幕小部件之間共享資料。
為了提供更豐富、更引人入勝的體驗,建議新增小部件預覽以包含在小部件選擇器中。 對於執行 Android 15 及更高版本的裝置,生成的動態小部件預覽允許使用者檢視目標小部件的動態和個性化版本,讓他們瞭解它在主螢幕上的顯示效果。 有關生成的動態小部件預覽以及舊裝置的備用選項,請檢視 將生成的預覽新增到您的部件選擇器 文件頁面。
資料庫和本地儲存
#如何訪問 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 支援效能分析、檢查堆、檢查 widget 樹、日誌診斷、除錯、觀察已執行程式碼行、除錯記憶體洩漏和記憶體碎片。有關更多資訊,請檢視 DevTools 文件。
通知
#如何設定推送通知?
#在 Android 中,您使用 Firebase Cloud Messaging 設定應用程式的推送通知。
在 Flutter 中,使用 Firebase Messaging 外掛訪問此功能。 有關使用 Firebase Cloud Messaging API 的更多資訊,請參閱 firebase_messaging 外掛文件。