Flutter 高頻面試題 20 問(含答案與實(shí)戰(zhàn)案例)
1. Flutter 的架構(gòu)分為哪幾層?各自作用是什么?
參考答案
Flutter 架構(gòu)自上而下分為三層:
Framework層(Dart)
包含 Widget、Rendering、Animation、Painting、Gestures 等庫。開發(fā)者直接與之交互,采用響應(yīng)式 UI 模式。Engine層(C++)
負(fù)責(zé)圖形渲染(Skia)、文本布局、Dart 運(yùn)行時(shí)管理、事件通道等。核心類:FlutterEngine。Embedder層(平臺(tái)特定)
將 Engine 嵌入到不同平臺(tái)(iOS、Android、Web、桌面),處理 Surface、線程、輸入事件等。
實(shí)際例子
在寫自定義繪制時(shí),CustomPaint 依賴 Framework 層的 RenderCustomPaint,最終調(diào)用 Engine 層的 Skia 引擎繪制路徑。
?? 注意事項(xiàng)
- 性能敏感操作避免在 Dart 層做大量計(jì)算,可考慮通過
Isolate或移至 Engine 層插件。 - 理解分層有助于定位問題:UI 卡頓先排查 Framework 層重建邏輯,再懷疑 Engine 線程阻塞。
2. Flutter 的 Widget、Element、RenderObject 三者關(guān)系?
參考答案
| 對象 | 角色 | 是否可變 | 生命周期 |
|---|---|---|---|
Widget |
配置描述(藍(lán)圖),輕量不可變 | 不可變 | 頻繁重建 |
Element |
實(shí)例化橋梁,持有 Widget 和 RenderObject 引用 | 可變 | 隨樹變化 |
RenderObject |
負(fù)責(zé)實(shí)際布局、繪制、命中測試 | 可變 | 需手動(dòng)管理 |
流程:
Widget → createElement() → Element → createRenderObject() → RenderObject
實(shí)際例子
-
Container是一個(gè)組合Widget,其Element可能是StatelessElement,而它內(nèi)部可能包含多個(gè)子RenderObject(如RenderDecoratedBox)。 - 當(dāng)父
Widget重建時(shí),Element通過canUpdate()對比新舊 Widget 的runtimeType和key,決定復(fù)用還是新建。
?? 注意事項(xiàng)
- 濫用
GlobalKey會(huì)強(qiáng)制保存Element狀態(tài),導(dǎo)致性能下降。 - 自定義
RenderObject時(shí)必須正確實(shí)現(xiàn)sizedByParent、performLayout等。
3. StatelessWidget 和 StatefulWidget 的生命周期對比
參考答案
| 階段 | StatelessWidget |
StatefulWidget |
|---|---|---|
| 構(gòu)造 | 直接 build
|
createState() → initState()
|
| 更新 | 重建時(shí)重新 build
|
didUpdateWidget() → build
|
| 銷毀 | 無 | dispose() |
| 依賴變化 | 無 | didChangeDependencies() |
實(shí)際例子
一個(gè)帶計(jì)數(shù)器的按鈕:
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
void initState() {
super.initState();
// 初始化監(jiān)聽、訂閱等
}
@override
void dispose() {
// 取消訂閱、釋放資源
super.dispose();
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() => _count++),
child: Text('$_count'),
);
}
}
4. Flutter 中的 Key 有什么作用?何時(shí)必須使用?
參考答案
Key 用于在 Widget 樹重建時(shí)幫助框架識(shí)別哪些 Element 可以復(fù)用、哪些需要替換。
-
LocalKey(ValueKey、ObjectKey、UniqueKey):同父級下唯一。 -
GlobalKey:全局唯一,可跨樹訪問 State 或 RenderObject。
實(shí)際例子
1.一個(gè)可重新排序的列表,當(dāng)刪除/新增條目時(shí),若不使用 Key,F(xiàn)lutter 可能錯(cuò)誤復(fù)用狀態(tài)。
ListView(
children: items.map((item) => MyItemWidget(
key: ValueKey(item.id), // 確保狀態(tài)與數(shù)據(jù)正確關(guān)聯(lián)
item: item,
)).toList(),
)
2.需要獲取子 Widget 位置或尺寸時(shí)
final globalKey = GlobalKey();
// ...
Container(key: globalKey);
// 獲取位置
RenderBox box = globalKey.currentContext?.findRenderObject() as RenderBox;
?? 注意事項(xiàng)
-
GlobalKey有性能開銷,非必要勿用。 - 當(dāng) Widget 的狀態(tài)需要跟隨數(shù)據(jù)移動(dòng)(如動(dòng)畫列表),必須使用 Key。
5. setState 的原理及調(diào)用后發(fā)生了什么?
參考答案
setState(fn) 主要做兩件事:
- 執(zhí)行傳入的回調(diào)函數(shù)
fn(通常修改狀態(tài)變量)。 - 標(biāo)記當(dāng)前
Element為 臟(dirty),在下一幀繪制時(shí)觸發(fā)build重建。
內(nèi)部流程
setState → markNeedsBuild → 調(diào)度 BuildOwner.scheduleBuildFor → 下一幀 WidgetsBinding.drawFrame → rebuild → performRebuild → build。
實(shí)際例子
setState(() {
_counter++; // 修改狀態(tài)
});
// 框架將在 16ms 內(nèi)重新調(diào)用 build 方法刷新 UI。
?? 注意事項(xiàng)
- 切勿在 setState 中執(zhí)行異步操作,因?yàn)榭蚣懿粫?huì)等待 Future 完成。
-
setState只在當(dāng)前 Widget 子樹內(nèi)觸發(fā)重建,若需要跨組件通信,使用狀態(tài)管理(Provider、Bloc 等)。
6. BuildContext 是什么?如何正確使用它?
參考答案
BuildContext 是 Widget 樹中 Element 的句柄,提供了以下能力:
- 獲取
Theme、MediaQuery、Navigator等 InheritedWidget 的數(shù)據(jù)。 - 查找父級 RenderObject 進(jìn)行布局測量。
- 作為
Navigator.push的上下文。
實(shí)際例子
final theme = Theme.of(context); // 獲取當(dāng)前主題
final size = MediaQuery.of(context).size; // 屏幕尺寸
Navigator.of(context).push(...); // 路由跳轉(zhuǎn)
?? 注意事項(xiàng)
- 異步回調(diào)中使用
context需檢查mounted,因?yàn)?Widget 可能已被銷毀。
Future.delayed(Duration(seconds: 1), () {
if (!mounted) return;
Navigator.of(context).pop(); // 安全調(diào)用
});
- 不要將
context保存為全局變量,它可能隨著樹重建而失效。
7. Flutter 中如何與原生平臺(tái)通信?列出三種 Channel 及其區(qū)別
參考答案
| Channel | 方向 | 特點(diǎn) | 使用場景 |
|---|---|---|---|
MethodChannel |
Dart ? 原生(異步) | 傳遞方法調(diào)用,有返回值 | 調(diào)用原生 API(如打開相機(jī)) |
EventChannel |
原生 → Dart(流) | 原生持續(xù)發(fā)送數(shù)據(jù)流 | 監(jiān)聽傳感器、網(wǎng)絡(luò)狀態(tài) |
BasicMessageChannel |
雙向消息 | 持久通信,支持自定義編解碼 | 高頻數(shù)據(jù)傳輸,如藍(lán)牙通信 |
實(shí)際例子
MethodChannel 獲取電池電量
Dart 端:
static const platform = MethodChannel('samples.flutter.dev/battery');
final batteryLevel = await platform.invokeMethod('getBatteryLevel');
Android 端 (Kotlin):
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
result.success(getBatteryLevel())
}
}
?? 注意事項(xiàng)
- 所有 Channel 操作必須在主線程(UI Thread)執(zhí)行,原生側(cè)回調(diào)需切換到主線程。
- 避免在短時(shí)間內(nèi)大量調(diào)用
MethodChannel,可考慮批量傳輸或使用BasicMessageChannel。
8. InheritedWidget 的工作原理及與 Provider 的關(guān)系?
參考答案
InheritedWidget 是一種特殊的 Widget,能將數(shù)據(jù)沿樹向下傳遞給依賴它的子孫 Widget。當(dāng)數(shù)據(jù)變化時(shí),會(huì)觸發(fā)依賴者的 didChangeDependencies 并重建。
實(shí)現(xiàn)步驟:
- 創(chuàng)建繼承
InheritedWidget的類,包含共享數(shù)據(jù)。 - 子 Widget 通過
context.dependOnInheritedWidgetOfExactType<T>()注冊依賴。 - 數(shù)據(jù)更新時(shí)調(diào)用
setState,通知所有依賴者重建。
與 Provider 關(guān)系
Provider 內(nèi)部基于 InheritedWidget 封裝,提供了更簡潔的語法、ChangeNotifier 集成和多數(shù)據(jù)源支持。
實(shí)際例子
手寫一個(gè)簡易主題共享:
class MyTheme extends InheritedWidget {
final Color primaryColor;
MyTheme({required this.primaryColor, required Widget child}) : super(child: child);
static MyTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyTheme>();
}
@override
bool updateShouldNotify(MyTheme old) => primaryColor != old.primaryColor;
}
?? 注意事項(xiàng)
- 使用
dependOnInheritedWidgetOfExactType會(huì)建立依賴關(guān)系;若僅讀取數(shù)據(jù)但不希望重建,改用getElementForInheritedWidgetOfExactType。 - 過多
InheritedWidget嵌套會(huì)影響性能,推薦使用 Provider 等上層封裝。
9. Flutter 動(dòng)畫實(shí)現(xiàn)方式有哪些?各自適用場景?
參考答案
| 方式 | 原理 | 適用場景 |
|---|---|---|
TweenAnimationBuilder |
內(nèi)置 Tween 與 AnimationController | 簡單補(bǔ)間動(dòng)畫(顏色、大小過渡) |
AnimatedContainer |
隱式動(dòng)畫,屬性變化自動(dòng)過渡 | 快速實(shí)現(xiàn)屬性動(dòng)畫 |
AnimatedBuilder + 自定義 Controller |
手動(dòng)控制動(dòng)畫進(jìn)度 | 復(fù)雜交互動(dòng)畫(如拖拽跟隨) |
Hero |
共享元素過渡 | 頁面間視覺連續(xù)過渡 |
Lottie / Rive |
播放預(yù)設(shè)計(jì)動(dòng)畫文件 | 設(shè)計(jì)師交付的復(fù)雜矢量動(dòng)畫 |
實(shí)際例子
AnimatedContainer 實(shí)現(xiàn)點(diǎn)擊放大:
bool _selected = false;
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _selected ? 200 : 100,
height: _selected ? 200 : 100,
child: GestureDetector(onTap: () => setState(() => _selected = !_selected)),
)
?? 注意事項(xiàng)
- 隱式動(dòng)畫內(nèi)部創(chuàng)建了
AnimationController,頻繁重建 Widget 可能導(dǎo)致控制器泄漏,需確保duration穩(wěn)定。 - 顯式動(dòng)畫需在
dispose中釋放AnimationController。
10. Flutter 渲染性能優(yōu)化有哪些常見手段?
參考答案
優(yōu)化維度及方法:
| 問題 | 解決方案 | 工具 |
|---|---|---|
| 過度重建 | 使用 const 構(gòu)造函數(shù)、拆分小 Widget、RepaintBoundary
|
Flutter Inspector |
| 復(fù)雜列表卡頓 |
ListView.builder 按需構(gòu)建、itemExtent 固定高度 |
DevTools Performance |
| 繪制復(fù)雜 | 用 CustomPaint 合并圖層、避免 saveLayer
|
debugRepaintRainbowEnabled |
| 圖片內(nèi)存 | 適當(dāng) cacheWidth / cacheHeight、ResizeImage
|
內(nèi)存快照 |
| 長列表滑動(dòng) | 使用 ScrollablePositionedList 定位、AutomaticKeepAliveClientMixin
|
--- |
實(shí)際例子
RepaintBoundary 隔離重繪區(qū)域:
RepaintBoundary(
child: AnimatedWidget(...), // 動(dòng)畫只重繪此子樹,不影響父級
)
?? 注意事項(xiàng)
-
Profile模式下測試性能,Debug 模式有額外檢查開銷。 -
Opacity與Clip操作會(huì)觸發(fā)saveLayer,應(yīng)優(yōu)先使用 **FadeTransition**或ClipRect等高效組件。
11. FutureBuilder 與 StreamBuilder 的區(qū)別和使用陷阱?
參考答案
| 對比項(xiàng) | FutureBuilder |
StreamBuilder |
|---|---|---|
| 數(shù)據(jù)源 | 單次異步任務(wù)(Future) |
持續(xù)數(shù)據(jù)流(Stream) |
| 重建時(shí)機(jī) |
Future 完成或出錯(cuò)時(shí) |
每次收到新數(shù)據(jù) |
| 狀態(tài) | ConnectionState.none/waiting/done |
ConnectionState.waiting/active/done |
實(shí)際例子
FutureBuilder<String>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return Text(snapshot.data ?? '');
},
)
?? 注意事項(xiàng)
- 不要在
FutureBuilder的builder外創(chuàng)建Future,否則每次重建都會(huì)重新發(fā)起請求。應(yīng)將其存儲(chǔ)在State或使用AsyncMemoizer。 -
StreamBuilder會(huì)持續(xù)訂閱,務(wù)必在 **dispose** 中取消訂閱。
12. Flutter 路由管理:Navigator 1.0 與 Navigator 2.0 區(qū)別?
參考答案
| 特性 | Navigator 1.0(命令式) | Navigator 2.0(聲明式) |
|---|---|---|
| 控制方式 |
push / pop 方法 |
基于 RouterDelegate + RouteInformationParser
|
| 適用場景 | 簡單頁面跳轉(zhuǎn) | 復(fù)雜導(dǎo)航(Web URL 同步、深層鏈接) |
| 狀態(tài)同步 | 手動(dòng)管理路由棧 | 框架根據(jù)應(yīng)用狀態(tài)自動(dòng)更新棧 |
實(shí)際例子
Navigator 2.0 簡化版(使用 go_router 庫):
GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => HomePage()),
GoRoute(path: '/detail/:id', builder: (_, state) => DetailPage(id: state.params['id']!)),
],
);
// 跳轉(zhuǎn)
context.go('/detail/123');
?? 注意事項(xiàng)
- 復(fù)雜應(yīng)用推薦使用第三方路由庫(
go_router、auto_route),避免手動(dòng)處理RouterDelegate的諸多細(xì)節(jié)。 - Navigator 2.0 需要理解
Page與Route的區(qū)別,Page是聲明,Route是實(shí)例。
13. Flutter 中如何做依賴注入?舉例說明
參考答案
依賴注入(DI)在 Flutter 中常用方式:
-
Provider/Riverpod:通過InheritedWidget實(shí)現(xiàn)樹級依賴。 -
get_it:服務(wù)定位器模式,全局單例訪問。 - 構(gòu)造函數(shù)注入:手動(dòng)傳遞依賴。
實(shí)際例子
使用 get_it + Injectable 代碼生成:
@injectable
class AuthService {
Future<void> login() async { ... }
}
@injectable
class UserRepository {
final AuthService authService;
UserRepository(this.authService);
}
// 初始化
GetIt getIt = GetIt.instance;
await configureDependencies(); // 生成代碼
// 使用
final userRepo = getIt<UserRepository>();
?? 注意事項(xiàng)
- 避免過度使用服務(wù)定位器導(dǎo)致隱藏依賴關(guān)系,優(yōu)先考慮構(gòu)造函數(shù)注入。
- 在 Widget 樹中,使用
Provider更符合 Flutter 響應(yīng)式模型,且能自動(dòng)處理生命周期。
14. Isolate 在 Flutter 中的作用是什么?如何使用?
參考答案
Flutter 是單線程事件循環(huán)模型,Isolate 是 Dart 的并發(fā)模型,每個(gè) Isolate 擁有獨(dú)立內(nèi)存堆和事件循環(huán),通過 消息傳遞 通信。
適用場景:
- 解析大型 JSON。
- 圖片壓縮/處理。
- 復(fù)雜數(shù)學(xué)計(jì)算(如加密)。
實(shí)際例子
使用 compute 函數(shù)簡化 Isolate:
int heavyTask(int value) {
// 耗時(shí)操作
return value * value;
}
final result = await compute(heavyTask, 42);
自定義 Isolate:
final receivePort = ReceivePort();
await Isolate.spawn(isolateEntry, receivePort.sendPort);
receivePort.listen((message) {
print('收到結(jié)果:$message');
});
?? 注意事項(xiàng)
-
compute每次調(diào)用都會(huì) 創(chuàng)建并銷毀 新 Isolate,開銷較大,不適合頻繁調(diào)用。頻繁任務(wù)應(yīng)使用長期存活的 Isolate 池。 - 不能在 Isolate 內(nèi)直接訪問 UI 相關(guān) API 或插件(需要通過
MethodChannel通信)。
15. Flutter 中如何處理深色模式(Dark Mode)?
參考答案
Flutter 通過 ThemeData 支持亮/暗主題切換。
步驟:
- 在
MaterialApp中定義theme和darkTheme。 - 使用
ThemeMode控制當(dāng)前模式(system/light/dark)。 - 子 Widget 通過
Theme.of(context)獲取動(dòng)態(tài)顏色。
實(shí)際例子
MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system, // 跟隨系統(tǒng)
home: MyHomePage(),
)
手動(dòng)切換:
Provider.of<ThemeProvider>(context).toggleTheme();
?? 注意事項(xiàng)
- 自定義顏色應(yīng)放在
ThemeData的extensions中,保證兩套主題一致性。 - 使用
CupertinoApp需分別設(shè)置theme和iosTheme。
16. Flutter 中常用的狀態(tài)管理方案對比?
參考答案
| 方案 | 特點(diǎn) | 適用規(guī)模 |
|---|---|---|
setState |
局部狀態(tài),簡單直接 | 小型 Widget 內(nèi)部 |
Provider |
官方推薦,基于 InheritedWidget | 中小型應(yīng)用 |
Riverpod |
編譯安全、無 Provider 嵌套地獄 | 中大型應(yīng)用 |
Bloc / Cubit |
事件驅(qū)動(dòng),嚴(yán)格單向數(shù)據(jù)流 | 復(fù)雜業(yè)務(wù)邏輯 |
GetX |
大而全,路由+依賴+狀態(tài)一體化 | 快速開發(fā),但爭議較多 |
實(shí)際例子
Riverpod 計(jì)數(shù)器:
final counterProvider = StateProvider<int>((ref) => 0);
class Counter extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
?? 注意事項(xiàng)
- 不要盲目追求復(fù)雜方案,簡單頁面用 setState 即可。
- 使用 GetX 需注意其破壞了 Flutter 的上下文依賴規(guī)則,可能導(dǎo)致測試?yán)щy。
17. 如何優(yōu)化 ListView 中復(fù)雜 Item 的滾動(dòng)流暢度?
參考答案
優(yōu)化清單:
| 手段 | 原理 | 實(shí)現(xiàn) |
|---|---|---|
ListView.builder |
按需構(gòu)建可見項(xiàng) |
itemBuilder 而非 children
|
| 固定高度 | 避免動(dòng)態(tài)測量開銷 |
itemExtent 或 prototypeItem
|
| 緩存 Widget | 避免重復(fù)創(chuàng)建 | 使用 const 構(gòu)造函數(shù) |
RepaintBoundary |
隔離重繪 | 包裹復(fù)雜 Item 內(nèi)容 |
| 圖片優(yōu)化 | 降低解碼壓力 | 設(shè)置 cacheWidth / cacheHeight
|
| 預(yù)加載 | 減少滑入時(shí)的空白 |
cacheExtent 適當(dāng)增大 |
實(shí)際例子
ListView.builder(
itemExtent: 80.0, // 已知每個(gè) Item 高度固定
itemBuilder: (context, index) {
return RepaintBoundary(
child: const MyComplexItem(), // 盡量 const
);
},
)
?? 注意事項(xiàng)
- 避免在
itemBuilder內(nèi)執(zhí)行setState或調(diào)用Navigator。 - 使用
ScrollController監(jiān)聽位置時(shí)記得dispose。
18. mixin 在 Flutter 中的應(yīng)用場景及與繼承的區(qū)別?
參考答案
mixin 用于在多個(gè)類中復(fù)用代碼,而無需繼承同一父類。Flutter 中大量使用 mixin,如 SingleTickerProviderStateMixin。
與繼承的區(qū)別:
- 繼承:單繼承,子類與父類強(qiáng)耦合。
- mixin:可以混入多個(gè),橫向復(fù)用,無父子關(guān)系。
實(shí)際例子
自定義日志 mixin:
mixin LoggerMixin {
void log(String msg) => print('[${runtimeType}]: $msg');
}
class MyWidget with LoggerMixin {
void doSomething() {
log('執(zhí)行操作'); // 可直接調(diào)用
}
}
?? 注意事項(xiàng)
mixin 無法聲明構(gòu)造函數(shù)。
注意 mixin 的線性化順序(with A, B中后混入的方法覆蓋前者)。
19. Flutter 的 WidgetsBindingObserver 作用及常用場景?
參考答案
WidgetsBindingObserver 用于監(jiān)聽?wèi)?yīng)用生命周期、系統(tǒng)設(shè)置變化(如字體縮放、深色模式)。
常用回調(diào):
-
didChangeAppLifecycleState:監(jiān)聽resumed/paused/inactive/detached。 -
didChangeMetrics:屏幕旋轉(zhuǎn)或鍵盤彈出。 -
didChangePlatformBrightness:系統(tǒng)深色模式切換。
實(shí)際例子
暫停視頻播放:
class _VideoPageState extends State<VideoPage> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_videoController.pause();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
?? 注意事項(xiàng)
- 務(wù)必在
dispose中移除觀察者,防止內(nèi)存泄漏。 -
didChangeMetrics觸發(fā)頻繁,避免在其中執(zhí)行重量操作。
20. Flutter 中如何實(shí)現(xiàn)一個(gè)自定義繪制組件?
參考答案
通過 CustomPaint 和 CustomPainter 實(shí)現(xiàn)。
步驟:
- 創(chuàng)建繼承
CustomPainter的類,實(shí)現(xiàn)paint和shouldRepaint。 - 在
paint中使用Canvas繪制圖形。 - 將
CustomPainter實(shí)例傳給CustomPaint的painter或foregroundPainter。
實(shí)際例子
繪制圓形進(jìn)度條:
class CircleProgressPainter extends CustomPainter {
final double progress;
CircleProgressPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
canvas.drawCircle(center, radius, paint);
// 繪制進(jìn)度弧
paint.color = Colors.red;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2,
2 * pi * progress,
false,
paint,
);
}
@override
bool shouldRepaint(covariant CircleProgressPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
// 使用
CustomPaint(
painter: CircleProgressPainter(0.7),
child: Center(child: Text('70%')),
)
?? 注意事項(xiàng)
- 在 **
shouldRepaint**中正確對比新舊參數(shù),避免不必要的重繪。 - 若需要響應(yīng)手勢,將
CustomPaint包裹在GestureDetector內(nèi)。 - 繪制文本時(shí)需注意
ParagraphBuilder或使用TextPainter。