為您的 Flutter 應用新增互動性
如何實現一個響應點選的有狀態元件。
如何修改應用以響應使用者輸入?在本教程中,您將為一個僅包含非互動式元件的應用新增互動性。具體來說,您將透過建立一個管理兩個無狀態元件的自定義有狀態元件,使圖示能夠響應點選。
構建佈局教程向您展示瞭如何建立以下截圖中的佈局。
佈局教程應用
當應用首次啟動時,星形圖示是實心的紅色,表示該湖泊之前已被收藏。星形旁邊的數字表示有 41 個人收藏了這個湖泊。完成本教程後,點選星形圖示會取消其收藏狀態,將實心星形替換為空心輪廓,並減少計數。再次點選則會收藏該湖泊,顯示為實心星形並增加計數。
為實現這一點,您將建立一個單一的自定義元件,其中包含星形和計數,它們本身也是元件。點選星形會改變兩個元件的狀態,因此同一個元件應該管理兩者。
您可以直接跳轉到 第 2 步:繼承 StatefulWidget 開始編寫程式碼。如果您想嘗試不同的狀態管理方式,請跳至 狀態管理。
有狀態(Stateful)和無狀態(Stateless)元件
#元件要麼是有狀態的,要麼是無狀態的。如果元件可以改變(例如,當用戶與其互動時),那麼它就是有狀態的。
無狀態(Stateless)元件從不改變。Icon、IconButton 和 Text 都是無狀態元件的示例。無狀態元件繼承自 StatelessWidget。
有狀態(Stateful)元件是動態的:例如,它可以根據使用者互動觸發的事件或接收資料時改變其外觀。Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是有狀態元件的示例。有狀態元件繼承自 StatefulWidget。
元件的狀態儲存在 State 物件中,將元件的狀態與其外觀分離開來。狀態由可變的值組成,例如滑塊的當前值或複選框是否被勾選。當元件的狀態改變時,狀態物件會呼叫 setState(),通知框架重新繪製元件。
建立有狀態元件
#在本節中,您將建立一個自定義有狀態元件。您將用一個單一的自定義有狀態元件替換兩個無狀態元件(實心紅星和旁邊的數值),該元件管理著包含兩個子元件的行:一個 IconButton 和一個 Text。
實現自定義有狀態元件需要建立兩個類
- 一個
StatefulWidget的子類,用於定義元件。 - 一個
State的子類,包含該元件的狀態並定義元件的build()方法。
本節將向您展示如何為湖泊應用構建一個名為 FavoriteWidget 的有狀態元件。設定完成後,第一步是選擇如何為 FavoriteWidget 管理狀態。
第 0 步:準備工作
#如果您已經完成了構建佈局教程中的應用,請跳至下一節。
- 確保您已經設定好了您的開發環境。
- 建立一個新的 Flutter 應用.
- 將
lib/main.dart檔案替換為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() 的函式引數在兩種狀態之間切換 UI
- 一個
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監聽所有點選事件。當用戶按下時,它新增高亮(實現為深綠色邊框)。當用戶釋放點選時,它移除高亮。- 在按下、鬆開或取消點選時呼叫
setState()以更新 UI,並且_highlight狀態發生變化。 - 在點選事件發生時,使用
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 食譜中的一個方案)中瞭解有關 GestureDetector 的更多資訊。
當您需要互動性時,最簡單的方法是使用預製元件之一。以下是部分列表
標準組件
#Material 元件
#資源
#以下資源可能有助於您為應用新增互動性。
手勢,Flutter 食譜中的一個章節。
- 處理手勢
如何建立按鈕並使其響應輸入。
- Flutter 中的手勢
Flutter 手勢機制的描述。
- Flutter API 文件
所有 Flutter 庫的參考文件。
- Wonderous 應用 執行應用 , 倉庫
具有自定義設計和引人入勝互動的 Flutter 展示應用。
- Flutter 的分層設計(影片)
-
此影片包含有關狀態和無狀態元件的資訊。由谷歌工程師 Ian Hickson 主講。