面向 Android 開發者的 Flutter
本文件旨在幫助 Android 開發者將其現有的 Android 知識應用於使用 Flutter 構建移動應用。如果您瞭解 Android 框架的基礎知識,那麼您可以將本文件作為 Flutter 開發的入門指南。
在 Flutter 中構建時,您的 Android 知識和技能非常有價值,因為 Flutter 依賴於移動作業系統來實現許多功能和配置。Flutter 是一種構建移動 UI 的新方式,但它有一個外掛系統可以與 Android (和 iOS) 進行非 UI 任務的通訊。如果您是 Android 專家,則無需重新學習所有內容即可使用 Flutter。
本文件可以像菜譜一樣使用,您可以隨意跳轉並查詢與您的需求最相關的問題。
檢視
#Flutter 中 View 的等價物是什麼?
#在 Android 中,View 是螢幕上顯示的一切的基礎。按鈕、工具欄和輸入,一切都是 View。在 Flutter 中,與 View 大致等價的是 Widget。Widget 不完全等同於 Android View,但在您熟悉 Flutter 的工作方式時,您可以將它們視為“宣告和構建 UI 的方式”。
然而,這些與 View 有一些區別。首先,Widget 的生命週期不同:它們是不可變的,只有在需要更改時才存在。每當 Widget 或其狀態更改時,Flutter 框架都會建立一個新的 Widget 例項樹。相比之下,Android View 只繪製一次,直到呼叫 invalidate 才重新繪製。
Flutter 的 Widget 輕量級,部分原因是它們的不可變性。因為它們本身不是 View,也不是直接繪製任何東西,而是一種 UI 及其語義的描述,在底層被“膨脹”成實際的 View 物件。
Flutter 包含 Material Components 庫。這些是實現 Material Design 指南的 Widget。Material Design 是一個靈活的設計系統,針對所有平臺進行了最佳化,包括 iOS。
但 Flutter 足夠靈活和富有表現力,可以實現任何設計語言。例如,在 iOS 上,您可以使用 Cupertino Widget 來生成類似 Apple 的 iOS 設計語言的介面。
如何更新小部件?
#在 Android 中,您透過直接改變檢視來更新檢視。然而,在 Flutter 中,Widget 是不可變的,不能直接更新,而是必須使用 Widget 的狀態。
這就是 Stateful 和 Stateless Widget 概念的來源。StatelessWidget 顧名思義,是一個沒有狀態資訊的 Widget。
當您描述的使用者介面部分除了物件中的配置資訊之外不依賴於任何其他內容時,StatelessWidget 非常有用。
例如,在 Android 中,這類似於放置帶有您徽標的 ImageView。徽標在執行時不會改變,因此在 Flutter 中使用 StatelessWidget。
如果您想根據 HTTP 呼叫或使用者互動後收到的資料動態更改 UI,那麼您必須使用 StatefulWidget 並告訴 Flutter 框架 Widget 的 State 已更新,以便它可以更新該 Widget。
這裡需要注意的重要一點是,無狀態和有狀態小部件的核心行為相同。它們每幀都會重建,不同之處在於 StatefulWidget 有一個 State 物件,它在幀之間儲存狀態資料並恢復它。
如果您有疑問,請始終記住這條規則:如果小部件發生變化(例如,由於使用者互動),則它是有狀態的。但是,如果小部件對變化做出反應,那麼如果它本身不響應變化,則包含它的父小部件仍然可以是無狀態的。
以下示例展示瞭如何使用 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
@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() 來動態新增或移除子檢視。在 Flutter 中,由於小部件是不可變的,因此沒有直接等同於 addChild() 的方法。相反,您可以向父檢視傳遞一個返回小部件的函式,並使用布林標誌控制該子檢視的建立。
例如,下面是如何在單擊 FloatingActionButton 時在兩個小部件之間切換
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 計算替換控制器預設線性運動的曲線。與小部件一樣,Flutter 中的動畫透過組合工作。
在構建小部件樹時,您將 Animation 分配給小部件的動畫屬性,例如 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 中繪製到畫布對於 Android 開發者來說是一項非常熟悉的任務。
Flutter 有兩個類可以幫助您繪製到畫布:CustomPaint 和 CustomPainter,後者實現您的演算法以繪製到畫布。
要了解如何在 Flutter 中實現簽名畫家,請參閱 Collin 在 自定義繪製上的回答。
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 中,透過組合較小的小部件(而不是擴充套件它們)來構建自定義小部件。這有點類似於在 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'));
}意圖
#Flutter 中 Intent 的等價物是什麼?
#在 Android 中,Intent 主要有兩個用例:在 Activity 之間導航,以及與元件通訊。另一方面,Flutter 沒有 Intent 的概念,儘管您仍然可以透過原生整合(使用外掛)啟動 Intent。
Flutter 並沒有真正直接等同於 activity 和 fragment 的概念;相反,在 Flutter 中,您可以使用 Navigator 和 Route 在螢幕之間導航,所有這些都在同一個 Activity 中。
Route 是應用程式的“螢幕”或“頁面”的抽象,而 Navigator 是管理路由的小部件。路由大致對映到 Activity,但它不具有相同的含義。導航器可以推送和彈出路由以在螢幕之間移動。導航器像堆疊一樣工作,您可以在其中 push() 要導航到的新路由,並從中 pop() 路由以“返回”。
在 Android 中,您在應用程式的 AndroidManifest.xml 中宣告您的活動。
在 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 的傳入意圖。
以下示例在執行我們的 Flutter 程式碼的原生 Activity 上註冊了一個文字共享意圖過濾器,以便其他應用可以與我們的 Flutter 應用共享文字。
基本流程意味著我們首先在 Android 原生端(在我們的 Activity 中)處理共享文字資料,然後等待 Flutter 請求資料,然後使用 MethodChannel 提供資料。
首先,在 AndroidManifest.xml 中註冊所有意圖的意圖過濾器
<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 中,處理意圖,從意圖中提取共享的文字,並儲存它。當 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);
}
}最後,當小部件渲染時,從 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 中使用過 async/await 正規化,或者使用過 Kotlin 的協程,那麼您可能對此很熟悉。
例如,您可以使用 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,這將觸發小部件子樹的重建並更新資料。
以下示例非同步載入資料並將其顯示在 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 密集型工作,使 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 更新其狀態,並在任務結束後將其隱藏。
在以下示例中,構建函式分為三個不同的函式。如果 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 小部件中
@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。
Activity 和 Fragment
#Flutter 中 Activity 和 Fragment 的等價物是什麼?
#在 Android 中,Activity 表示使用者可以執行的單個重點任務。Fragment 表示一種行為或使用者介面的一部分。Fragment 是一種模組化程式碼、為更大螢幕組合複雜使用者介面並幫助擴充套件應用程式 UI 的方式。在 Flutter 中,這兩個概念都屬於 Widget 的範疇。
要了解有關構建 Activity 和 Fragment UI 的更多資訊,請參閱社群貢獻的 Medium 文章面向 Android 開發者的 Flutter:如何在 Flutter 中設計 Activity UI。
如意圖部分所述,Flutter 中的螢幕由 Widget 表示,因為 Flutter 中一切都是小部件。使用 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 用於線性佈局您的小部件——無論是水平還是垂直。在 Flutter 中,使用 Row 或 Column 小部件來實現相同的結果。
如果您注意到兩個程式碼示例除了“Row”和“Column”小部件外是相同的。子級是相同的,並且可以利用此功能來開發可以隨著時間變化且具有相同子級的豐富佈局。
@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 中為小部件新增 onClick 監聽器?
#在 Android 中,您可以透過呼叫“setOnClickListener”方法將 onClick 附加到按鈕等檢視。
在 Flutter 中,有兩種新增觸控監聽器的方法
- 如果小部件支援事件檢測,則將函式傳遞給它並在函式中處理它。例如,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),
),
),
);
}
}如何處理小部件上的其他手勢?
#使用 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 中,您建立一個介面卡並將其傳遞給 ListView,ListView 會使用介面卡返回的內容渲染每一行。但是,您必須確保回收您的行,否則,您會遇到各種瘋狂的視覺故障和記憶體問題。
由於 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 上,您更新介面卡並呼叫 notifyDataSetChanged。
在 Flutter 中,如果您在 setState() 中更新小部件列表,您很快就會發現您的資料在視覺上沒有變化。這是因為當呼叫 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 介面卡中的 getView 函式;它接受一個位置,並返回您想要在該位置渲染的行。
最後,但最重要的是,請注意 onTap() 函式不再重新建立列表,而是 .add 到列表中。
處理文字
#如何為我的文字小部件設定自定義字型?
#在 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 Widget 上自定義其他樣式元素。Text Widget 的 style 引數接受一個 TextStyle 物件,您可以在其中自定義許多引數,例如
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
表單輸入
#有關使用表單的更多資訊,請參閱檢索文字欄位的值。
Input 中“提示”的等價物是什麼?
#在 Flutter 中,您可以透過為 Text Widget 的裝飾建構函式引數新增 InputDecoration 物件,輕鬆地為您的輸入顯示“提示”或佔位符文字。
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)如何顯示驗證錯誤?
#就像您使用“提示”一樣,將 InputDecoration 物件傳遞給文字小部件的裝飾建構函式。
但是,您不希望一開始就顯示錯誤。相反,當用戶輸入無效資料時,更新狀態,並傳遞一個新的 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 RTDBfirebase_storage用於 Firebase 雲端儲存firebase_messaging用於 Firebase Messaging (FCM)flutter_firebase_ui用於快速 Firebase 身份驗證整合(Facebook、Google、Twitter 和電子郵件)cloud_firestore用於 Firebase 雲 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 作為您的應用 Widget,它提供了一些相同的功能,但不如 MaterialApp 豐富。
要自定義任何子元件的顏色和樣式,請將 ThemeData 物件傳遞給 MaterialApp 小部件。例如,在下面的程式碼中,種子顏色方案設定為深紫色,文字選擇顏色設定為紅色。
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(),
);
}
}主螢幕小部件
#如何建立主螢幕小部件?
#Android 主螢幕小部件不能完全使用 Flutter 建立。它們必須使用 Jetpack Glance(首選方法)或 XML 佈局程式碼。使用第三方包 home_widget,您可以將主螢幕小部件連線到 Dart 程式碼,在宿主小部件中嵌入 Flutter 元件(作為影像),並與主螢幕小部件共享資料(從 Flutter 或到 Flutter)。
資料庫和本地儲存
#如何訪問 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 外掛文件。