為你的 Flutter 應用加入互動體驗
如何修改您的應用程式以使其對使用者輸入做出反應?在本課程中,您將為僅包含非互動式 widget 的應用程式新增互動性。具體來說,您將透過建立一個管理兩個無狀態 widget 的自訂有狀態 widget,修改一個圖示實現使其可點選。
建構佈局課程 中展示瞭如何建構下面截圖所示的佈局。

當應用第一次啟動時,這個星形圖示是實心紅色,表明這個湖以前已經被收藏過了。星號旁邊的數字表示 41 個人已經收藏了此湖。完成本課程後,點選星形圖示將取消收藏狀態,然後用輪廓線的星形圖示代替實心的,並減少計數。再次點選會重新收藏,並增加計數。
為了實現這個,您將建立一個包含星形圖示和計數的自訂 widget,它們都是 widget。因為點選星形圖示會更改這兩個 widget 的狀態,所以同一個 widget 應該同時管理這兩個 widget。
您可以直接檢視 第二步: 建立 StatefulWidget 的子類別。如果您想嘗試不同的管理狀態方式,請跳至 狀態管理。
有狀態和無狀態的 widgets
有些 widgets 是有狀態的, 有些是無狀態的。如果使用者與 widget 互動,widget 會發生變化,那麼它就是 有狀態的。
無狀態的 widget 自身無法改變。
Icon
、IconButton
和 Text
都是無狀態 widget,它們都是 StatelessWidget
的子類別。
而 有狀態的 widget 自身是可動態改變的(基於State)。例如,可以透過與使用者的互動或是隨著資料的改變而導致外觀形態的變化。
Checkbox
、Radio
、Slider
、
InkWell
、Form
和 TextField
都是有狀態 widget,它們都是 StatefulWidget
的子類別。
一個 widget 的狀態儲存在一個 State
物件中,它和 widget 的顯示分離。
Widget 的狀態是一些可以更改的值,如一個滑動條的當前值或一個複選框是否被選中。當 widget 狀態改變時,State 物件呼叫 setState()
,告訴框架去重繪 widget。
建立一個有狀態的 widget
在本節中,您將建立一個自訂有狀態的 widget。您將使用一個自訂有狀態 widget 來替換兩個無狀態 widget——
紅色實心星形圖示和其旁邊的數字計數——
該 widget 用兩個子 widget 管理一行 IconButton
和 Text
。
實現一個自訂的有狀態 widget 需要建立兩個類:
-
一個 StatefulWidget 的子類別,用來定義一個 widget 類別。
-
一個
State
的子類別,包含該widget狀態並定義該 widget 的build()
方法。
這一節展示如何為 Lakes 應用程式建構一個名為
FavoriteWidget
的 StatefulWidget。第一步是選擇如何管理 FavoriteWidget
的狀態。
步驟 0: 開始
如果你已經在 建構佈局課程(第 6 步) 中成功建立了應用程式,你可以跳過下面的部分。
-
確保你已經 設定 好了你的環境。
-
用 GitHub 上的
main.dart
替換lib/main.dart
檔案。 -
用 GitHub 上的
pubspec.yaml
替換pubspec.yaml
檔案。 -
在你的工程中建立一個
images
資料夾,並新增lake.jpg
。
如果你有一個連線並可用的裝置,或者你已經啟動了 iOS 模擬器 或者 Android 模擬器 (Flutter 安裝部分介紹過),你就可以開始了!
Step 1: 決定哪個物件管理 widget 的狀態
一個 widget 的狀態可以透過多種方式進行管理,但在我們的範例中,widget 本身
——FavoriteWidget
——
將管理自己的狀態。在這個例子中,切換星形圖示是一個獨立的操作,不會影響父視窗 widget 或其他使用者介面,因此該 widget 可以在內部處理它自己的狀態。
你可以在 狀態管理 中瞭解更多關於 widget 和狀態的分離以及如何管理狀態的資訊。
Step 2: 建立 StatefulWidget 的子類別
FavoriteWidget
類管理自己的狀態,因此它透過重寫 createState()
來建立狀態物件。框架會在建構 widget 時呼叫 createState()
。在這個例子中,createState()
建立 _FavoriteWidgetState
的例項,您將在下一步中實現該例項。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
Step 3: 建立 State 的子類別
_FavoriteWidgetState
類儲存可變資訊;可以在 widget 的生命週期內改變邏輯和內部狀態。當應用第一次啟動時,使用者介面顯示一個紅色實心的星星形圖示,表明該湖已經被收藏,並有 41 個「喜歡」。狀態物件儲存這些資訊在 _isFavorited
和 _favoriteCount
變數中。
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
// ···
}
狀態物件也定義了 build()
方法。這個 build()
方法建立一個包含紅色 IconButton
和 Text
的行。該 widget 使用 IconButton
(而不是 Icon
),因為它具有一個 onPressed
屬性,該屬性定義了處理點選的回呼(Callback)方法 (_toggleFavorite
)。你將會在接下來的步驟中嘗試定義它。
class _FavoriteWidgetState extends State<FavoriteWidget> {
// ···
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(0),
child: IconButton(
padding: const EdgeInsets.all(0),
alignment: Alignment.centerRight,
icon: (_isFavorited
? const Icon(Icons.star)
: const Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: SizedBox(
child: Text('$_favoriteCount'),
),
),
],
);
}
}
按下 IconButton
時會呼叫 _toggleFavorite()
方法,然後它會呼叫 setState()
。呼叫 setState()
是至關重要的,因為這告訴框架,
widget 的狀態已經改變,應該重繪。
setState()
在如下兩種狀態中切換 UI:
-
實心的星形圖示和數字 41
-
輪廓線的星形圖示
star_border
和數字 40 之間切換 UI
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
Step 4: 將有 stateful widget 插入 widget 樹中
將您自訂 stateful widget 在 build()
方法中新增到 widget 樹中。首先,找到建立 Icon
和 Text
的程式碼,並刪除它,在相同的位置建立有狀態的 widget:
@@ -10,2 +5,2 @@
|
|
10
5
|
class MyApp extends StatelessWidget {
|
11
6
|
const MyApp({super.key});
|
@@ -40,11 +35,7 @@
|
|
40
35
|
],
|
41
36
|
),
|
42
37
|
),
|
43
|
-
|
44
|
-
Icons.star,
|
45
|
-
color: Colors.red[500],
|
46
|
-
),
|
47
|
-
const Text('41'),
|
38
|
+
const FavoriteWidget(),
|
48
39
|
],
|
49
40
|
),
|
50
41
|
);
|
就是這樣!當您熱重載應用後,星形圖示就會響應點選了。
有問題?
如果您的程式碼無法執行,請在 IDE 中查詢可能的錯誤。 除錯 Flutter 應用程式 可能會有所幫助。如果仍然無法找到問題,請根據 GitHub 上的範例檢查程式碼。
如果您仍有問題,可以諮詢 社群 中的任何一位開發者。
本頁面的其餘部分介紹了可以管理 widget 狀態的幾種方式,並列出了其他可用的可互動的 widget。
狀態管理
誰管理著 stateful widget 的狀態?widget 本身?父 widget?雙方?另一個物件?答案是…… 這取決於實際情況。有幾種有效的方法可以給你的 widget 加入互動。作為 widget 設計師,你可以基於你所期待的表現 widget 的方式來做決定。以下是一些管理狀態的最常見的方法:
如何決定使用哪種管理方法?以下原則可以幫助您決定:
-
如果狀態是使用者資料,如複選框的選中狀態、滑塊的位置,則該狀態最好由父 widget 管理。
-
如果所討論的狀態是有關介面外觀效果的,例如動畫,那麼狀態最好由 widget 本身來管理。
如果有疑問,首選是在父 widget 中管理狀態。
我們將透過建立三個簡單範例來舉例說明管理狀態的不同方式:
TapboxA、TapboxB 和 TapboxC。這些例子功能是相似的——
每建立一個容器,當點選時,在綠色或灰色框之間切換。
_active
確定顏色:綠色為 true,灰色為 false。


這些範例使用 GestureDetector
捕獲 Container
上的使用者動作。
widget 管理自己的狀態
有時,widget 在內部管理其狀態是最好的。例如,當 ListView
的內容超過渲染框時,
ListView 自動滾動。大多數使用 ListView
的開發人員不想管理 ListView
的滾動行為,因此 ListView
本身管理其滾動偏移量。
_TapboxAState
類:
-
管理
TapboxA
的狀態。 -
定義布林值
_active
確定盒子的當前顏色。 -
定義
_handleTap()
函式,該函式在點選該盒子時更新_active
,並呼叫setState()
更新 UI。 -
實現 widget 的所有互動式行為。
import 'package:flutter/material.dart';
// TapboxA manages its own state.
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
const TapboxA({super.key});
@override
State<TapboxA> createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32.0, color: Colors.white),
),
),
),
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: const Center(
child: TapboxA(),
),
),
);
}
}
父 widget 管理 widget 的 state
一般來說父 widget 管理狀態並告訴其子 widget 何時更新通常是最合適的。例如,IconButton
允許您將圖示視為可點按的按鈕。
IconButton
是一個無狀態 widget,因為我們認為父 widget 需要知道該按鈕是否被點選來採取相應的處理。
在以下範例中,TapboxB 透過回呼(Callback)將其狀態到其父類別。由於 TapboxB 不管理任何狀態,因此它繼承自 StatelessWidget。
ParentWidgetState 類:
-
為 TapboxB 管理
_active
狀態; -
實現
_handleTapboxChanged()
,當盒子被點選時呼叫的方法; -
當狀態改變時,呼叫
setState()
更新 UI。
TapboxB 類:
-
繼承 StatelessWidget 類,因為所有狀態都由其父 widget 處理;
-
當檢測到點選時,它會通知父 widget。
import 'package:flutter/material.dart';
// ParentWidget manages the state for TapboxB.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
const TapboxB({
super.key,
this.active = false,
required this.onChanged,
});
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32.0, color: Colors.white),
),
),
),
);
}
}
混搭管理
對於一些 widget 來說,混搭管理的方法最合適的。在這種情況下,有狀態的 widget 自己管理一些狀態,同時父 widget 管理其他方面的狀態。
在 TapboxC
範例中,點選時,盒子的周圍會出現一個深綠色的邊框。點選時,邊框消失,盒子的顏色改變。
TapboxC
將其 _active
狀態匯出到其父 widget 中,但在內部管理其 _highlight
狀態。這個例子有兩個狀態物件 _ParentWidgetState
和 _TapboxCState
。
_ParentWidgetState
物件:
-
管理
_active
狀態。 -
實現
_handleTapboxChanged()
,此方法在盒子被點選時呼叫。 -
當點選盒子並且
_active
狀態改變時呼叫setState()
來更新 UI。
_TapboxCState
物件:
-
管理
_highlight
state。 -
GestureDetector
監聽所有 tap 事件。當用戶點下時,它新增高亮(深綠色邊框);當用戶釋放時,會移除高亮。 -
當按下、抬起、或者取消點選時更新
_highlight
狀態,呼叫setState()
更新UI。 -
當點選時,
widget
屬性將狀態的改變傳遞給父 widget 並進行合適的操作。
import 'package:flutter/material.dart';
//---------------------------- ParentWidget ----------------------------
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
const TapboxC({
super.key,
this.active = false,
required this.onChanged,
});
final bool active;
final ValueChanged<bool> onChanged;
@override
State<TapboxC> createState() => _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
@override
Widget build(BuildContext context) {
// This example adds a green border on tap down.
// On tap up, the square changes to the opposite state.
return GestureDetector(
onTapDown: _handleTapDown, // Handle the tap events in the order that
onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700]!,
width: 10.0,
)
: null,
),
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32.0, color: Colors.white)),
),
),
);
}
}
另一種實現可能會將高亮狀態匯出到父 widget,同時保持 active 狀態為內部,但如果你要求某人使用該 TapBox,他們可能會抱怨說沒有多大意義。開發人員只會關心該框是否處於活動狀態。開發人員可能不在乎高亮顯示是如何管理的,並且傾向於讓 TapBox 處理這些細節。
其他互動式 widgets
Flutter 提供各種按鈕和類似的互動式 widget。這些 widget 中的大多數都實現了 Material Design guidelines,它們定義了一組具有質感的 UI 元件。
如果你願意,你可以使用 GestureDetector
來給任何自訂 widget 新增互動性。你可以在 管理狀態 中找到 GestureDetector
的範例。同時你也可以在 Flutter cookbook 的 處理點選
中學習更多關於 GestureDetector
的內容。
當你需要互動性時,最容易的是使用預製的 widget。這是預置 widget 部分列表:
標準 widgets
Material 元件
Checkbox
DropdownButton
TextButton
FloatingActionButton
IconButton
Radio
ElevatedButton
Slider
Switch
TextField
資源
以下資源可能會在給您的應用新增互動的時候有所幫助。
手勢,Flutter 實用課程 裡的一個小節。
處理手勢,widgets 介紹 文件中一部分
如何建立一個按鈕並使其響應使用者動作。
點選、拖動和其他手勢
Flutter 手勢機制的描述。
Flutter API 文件
所有 Flutter API 的參考文件。
Flutter Gallery 應用,程式碼儲存庫
一個 Demo 應用程式,展示了許多 Material 和其他 Flutter 功能。
Flutter 的分層設計 (影片)
此影片包含有關有狀態和無狀態 widget 的資訊。由 Google 工程師 Ian Hickson 講解。