為你的 Flutter 應用新增互動性
如何實現響應點選的有狀態元件。
你如何修改你的應用使其對使用者輸入做出反應?在本教程中,你將為僅包含非互動式元件的應用新增互動性。具體來說,你將修改一個圖示,使其可點選,透過建立一個管理兩個無狀態元件的自定義有狀態元件來實現。
《構建佈局教程》向你展示瞭如何建立以下螢幕截圖的佈局。
佈局教程應用
當應用首次啟動時,星星是純紅色,表明該湖已被收藏。星星旁邊的數字表明有 41 人收藏了該湖。完成本教程後,點選星星將移除其收藏狀態,將純星星替換為輪廓並減少計數。再次點選將收藏該湖,繪製純星星並增加計數。
為了實現這一點,你將建立一個單一的自定義元件,其中包含星星和計數,它們本身也是元件。點選星星會改變兩個元件的狀態,因此應該由同一個元件管理兩者。
你可以直接進入 步驟 2:繼承 StatefulWidget 開始編寫程式碼。如果你想嘗試不同的狀態管理方式,請跳到 管理狀態。
有狀態和無狀態元件
#一個元件要麼是有狀態的,要麼是無狀態的。如果一個元件可以改變——例如,當用戶與其互動時——那麼它是有狀態的。
一個無狀態元件永遠不會改變。Icon、IconButton 和 Text 是無狀態元件的例子。無狀態元件繼承自 StatelessWidget。
一個有狀態元件是動態的:例如,它可以根據使用者互動觸發的事件或接收到資料時改變其外觀。Checkbox、Radio、Slider、InkWell、Form 和 TextField 是有狀態元件的例子。有狀態元件繼承自 StatefulWidget。
元件的狀態儲存在一個 State 物件中,將元件的狀態與其外觀分離。狀態由可以改變的值組成,例如滑塊的當前值或複選框是否被選中。當元件的狀態改變時,狀態物件呼叫 setState(),告訴框架重新繪製元件。
建立有狀態元件
#在本節中,你將建立一個自定義有狀態元件。你將用一個單一的自定義有狀態元件替換兩個無狀態元件——純紅色星星和星星旁邊的數字計數——該元件管理一個包含兩個子元件的行:一個 IconButton 和一個 Text。
實現自定義有狀態元件需要建立兩個類
- 一個繼承自
StatefulWidget的子類,用於定義元件。 - 一個繼承自
State的子類,包含該元件的狀態並定義元件的build()方法。
本節展示瞭如何為湖泊應用構建一個名為 FavoriteWidget 的有狀態元件。設定好後,你的第一步是選擇如何管理 FavoriteWidget 的狀態。
步驟 0:準備就緒
#如果你已經在《構建佈局教程》中構建了該應用,請跳到下一節。
- 確保你已經 設定 了你的環境。
- 建立一個新的 Flutter 應用.
- 用
main.dart替換lib/main.dart檔案。 - 用
pubspec.yaml替換pubspec.yaml檔案。 - 在你的專案中建立一個
images目錄,並新增lake.jpg。
一旦你擁有一個已連線並啟用的裝置,或者你已經啟動了 iOS 模擬器(Flutter 安裝的一部分)或 Android 模擬器(Android Studio 安裝的一部分),你就可以開始了!
步驟 1:確定哪個物件管理元件的狀態
#狀態管理有多種方式,但在我們的例子中,元件本身,FavoriteWidget,將管理自己的狀態。在本例中,切換星星是一個孤立的操作,不會影響父元件或 UI 的其餘部分,因此元件可以內部處理其狀態。
更多關於元件和狀態分離以及狀態管理方式的資訊,請參閱 管理狀態。
步驟 2:繼承 StatefulWidget
#FavoriteWidget 類管理自己的狀態,因此它重寫 createState() 以建立一個 State 物件。當框架想要構建元件時,會呼叫 createState()。在本例中,createState() 返回一個 _FavoriteWidgetState 的例項,你將在下一步中實現它。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
步驟 3:繼承 State
#_FavoriteWidgetState 類儲存可以在元件生命週期內改變的可變資料。當應用首次啟動時,UI 顯示一個純紅色星星,表明該湖具有“收藏”狀態,以及 41 個點贊。這些值儲存在 _isFavorited 和 _favoriteCount 欄位中
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
該類還定義了一個 build() 方法,該方法建立一個包含紅色 IconButton 和 Text 的行。你使用 IconButton(而不是 Icon),因為它具有一個 onPressed 屬性,該屬性定義了處理點選的回撥函式(_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.center,
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() 至關重要,因為這告訴框架元件的狀態已更改,並且應該重新繪製元件。setState() 的函式引數在以下兩種狀態之間切換
- 一個
star圖示和數字 41 - 一個
star_border圖示和數字 40
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
步驟 4:將有狀態元件插入到元件樹中
#將你的自定義有狀態元件新增到應用 build() 方法中的元件樹中。首先,找到建立 Icon 和 Text 的程式碼,並將其刪除。在相同的位置,建立有狀態元件
child: Row(
children: [
// ...
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
const FavoriteWidget(),
],
),
就是這樣!當你熱過載應用時,星星圖示現在應該可以響應點選了。
有問題嗎?
#如果你的程式碼無法執行,請在你的 IDE 中查詢可能的錯誤。除錯 Flutter 應用 可能會有所幫助。如果你仍然找不到問題,請將你的程式碼與 GitHub 上的互動式湖泊示例進行比較。
如果你仍然有疑問,請參考開發者 社群 的任何一個渠道。
本頁的其餘部分涵蓋了元件狀態管理的幾種方式,並列出了其他可用的互動式元件。
狀態管理
#誰管理有狀態元件的狀態?元件本身?父元件?兩者?另一個物件?答案是……這取決於。有幾種有效的方法可以使你的元件具有互動性。你作為元件設計者根據你期望元件的使用方式做出決定。以下是管理狀態的最常見方法
你如何決定使用哪種方法?以下原則應該可以幫助你做出決定
-
如果相關狀態是使用者資料,例如複選框的選中或未選中模式,或者滑塊的位置,那麼最好由父元件管理該狀態。
-
如果相關狀態是美觀的,例如動畫,那麼最好由元件本身管理該狀態。
如果你不確定,請從在父元件中管理狀態開始。
我們將透過建立三個簡單的示例:TapboxA、TapboxB 和 TapboxC 來演示不同的狀態管理方式。這些示例的工作方式類似——每個示例建立一個容器,當點選時會在綠色或灰色框之間切換。_active 布林值確定顏色:綠色表示活動狀態或灰色表示非活動狀態。
這些示例使用 GestureDetector 來捕獲 Container 上的活動。
元件管理自己的狀態
#有時,元件自己管理狀態是最有意義的。例如,ListView 會在內容超出渲染框時自動滾動。大多數使用 ListView 的開發人員不希望管理 ListView 的滾動行為,因此 ListView 本身管理其滾動偏移量。
_TapboxAState 類
- 管理
TapboxA的狀態。 - 定義
_active布林值,該值確定框的當前顏色。 - 定義
_handleTap()函式,該函式在點選框時更新_active並呼叫setState()函式來更新 UI。 - 實現元件的所有互動行為。
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,
height: 200,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, 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()),
),
);
}
}
父元件管理元件的狀態
#通常,讓父元件管理狀態並告訴其子元件何時更新是最有意義的。例如,IconButton 允許你將圖示視為可點選的按鈕。IconButton 是一個無狀態元件,因為我們決定父元件需要知道按鈕是否被點選,以便它可以採取適當的操作。
在下面的示例中,TapboxB 透過回撥將其狀態匯出到其父元件。由於 TapboxB 不管理任何狀態,因此它繼承自 StatelessWidget。
ParentWidgetState 類
- 管理
TapboxB的_active狀態。 - 實現
_handleTapboxChanged(),在點選框時呼叫的方法。 - 當狀態改變時,呼叫
setState()來更新 UI。
TapboxB 類
- 繼承自 StatelessWidget,因為所有狀態都由其父元件處理。
- 檢測到點選時,它會通知父元件。
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,
height: 200,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
);
}
}
混合搭配方法
#對於某些元件,混合搭配方法最有意義。在這種情況下,有狀態元件管理部分狀態,而父元件管理狀態的其他方面。
在 TapboxC 示例中,按下時,方框周圍會出現深綠色的邊框。鬆開時,邊框消失,方框的顏色會改變。TapboxC 將其 _active 狀態匯出到其父元件,但內部管理其 _highlight 狀態。此示例有兩個 State 物件,分別是 _ParentWidgetState 和 _TapboxCState。
_ParentWidgetState 物件
- 管理
_active狀態。 - 實現
_handleTapboxChanged(),在點選框時呼叫的方法。 - 當發生點選事件且
_active狀態發生變化時,呼叫setState()來更新 UI。
_TapboxCState 物件
- 管理
_highlight狀態。 GestureDetector監聽所有點選事件。當用戶按下時,它會新增高亮(實現為深綠色邊框)。當用戶鬆開點選時,它會移除高亮。- 在按下、鬆開或取消點選時,如果
_highlight狀態發生變化,則呼叫setState()來更新 UI。 - 在點選事件發生時,將該狀態變化傳遞給父元件,以便使用
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,
height: 200,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(color: Colors.teal[700]!, width: 10)
: null,
),
child: Center(
child: Text(
widget.active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
);
}
}
另一種實現方式可以將高亮狀態匯出到父元件,同時將活動狀態保留在內部,但如果你要求某人使用該點選框,他們可能會抱怨它沒有太大意義。開發者關心方框是否處於活動狀態。開發者可能不關心如何管理高亮,並且更喜歡點選框處理這些細節。
其他互動式元件
#Flutter 提供了各種按鈕和類似的互動式元件。其中大多陣列件實現了 Material Design 指南,該指南定義了一組具有明確 UI 的元件。
如果你更喜歡,可以使用 GestureDetector 為任何自定義元件構建互動性。你可以在 管理狀態 中找到 GestureDetector 的示例。在 Flutter cookbook 的 處理點選 中瞭解更多關於 GestureDetector 的資訊。
當你需要互動性時,使用其中一個預製元件是最簡單的。
標準組件
#Material 元件
#資源
#以下資源可能有助於你在應用中新增互動性。
手勢,Flutter cookbook 中的一部分。
- 處理手勢
如何建立一個按鈕並使其響應輸入。
- Flutter 中的手勢
Flutter 手勢機制的描述。
- Flutter API 文件
所有 Flutter 庫的參考文件。
- 精彩應用 正在執行的應用,倉庫
具有自定義設計和引人入勝互動的 Flutter 演示應用。
- Flutter 的分層設計 (影片)
-
此影片包含有關狀態和無狀態元件的資訊。由 Google 工程師 Ian Hickson 講解。