為您的 Flutter 應用程式新增互動功能
您要如何修改您的應用程式,使其能對使用者輸入做出反應?在本教學課程中,您將為只包含非互動式 widget 的應用程式新增互動功能。具體來說,您將修改圖示,透過建立管理兩個無狀態 widget 的自訂狀態型 widget,使其成為可點擊的圖示。
建構版面配置教學課程向您展示如何建立以下螢幕截圖的版面配置。
當應用程式首次啟動時,星星會是實心的紅色,表示此湖泊之前已加入最愛。星星旁邊的數字表示有 41 個人已將此湖泊加入最愛。完成本教學課程後,點擊星星會移除其最愛狀態,將實心星星替換為外框並減少計數。再次點擊會將湖泊加入最愛,繪製實心星星並增加計數。
為此,您將建立一個包含星星和計數的單一自訂 widget,它們本身也是 widget。點擊星星會變更兩個 widget 的狀態,因此應該由同一個 widget 管理這兩者。
您可以直接進入 步驟 2:子類別 StatefulWidget 開始撰寫程式碼。如果您想嘗試不同的狀態管理方式,請跳至管理狀態。
有狀態和無狀態 Widget
#widget 可能是狀態型或無狀態。如果 widget 可以變更(例如,當使用者與其互動時),則它是狀態型 widget。
無狀態 widget 永遠不會變更。Icon
、IconButton
和 Text
是無狀態 widget 的範例。無狀態 widget 是 StatelessWidget
的子類別。
狀態型 widget 是動態的:例如,它可以根據使用者互動觸發的事件或在收到資料時變更其外觀。Checkbox
、Radio
、Slider
、InkWell
、Form
和 TextField
是狀態型 widget 的範例。狀態型 widget 是 StatefulWidget
的子類別。
widget 的狀態儲存在 State
物件中,將 widget 的狀態與其外觀分開。狀態包含可以變更的值,例如滑桿的目前值或核取方塊是否已選取。當 widget 的狀態變更時,狀態物件會呼叫 setState()
,告知架構重新繪製 widget。
建立有狀態 Widget
#在本節中,您將建立自訂狀態型 widget。您將使用管理包含兩個子 widget 的列的單一自訂狀態型 widget 來取代兩個無狀態 widget(實心紅色星星和旁邊的數字計數):IconButton
和 Text
。
實作自訂狀態型 widget 需要建立兩個類別
- 定義 widget 的
StatefulWidget
子類別。 - 包含該 widget 狀態並定義 widget
build()
方法的State
子類別。
本節說明如何為湖泊應用程式建立名為 FavoriteWidget
的狀態型 widget。設定完成後,您的第一個步驟是選擇如何管理 FavoriteWidget
的狀態。
步驟 0:準備就緒
#如果您已經在建構版面配置教學課程中建立應用程式,請跳至下一節。
- 請確保您已設定您的環境。
- 建立新的 Flutter 應用程式.
- 將
lib/main.dart
檔案替換為main.dart
。 - 將
pubspec.yaml
檔案替換為pubspec.yaml
。 - 在您的專案中建立
images
目錄,並新增lake.jpg
。
一旦您連線並啟用裝置,或您已啟動iOS 模擬器(Flutter 安裝的一部分)或Android 模擬器(Android Studio 安裝的一部分),您就可以開始了!
步驟 1:決定哪個物件管理 Widget 的狀態
#widget 的狀態可以透過幾種方式管理,但在我們的範例中,widget 本身 FavoriteWidget
將會管理自己的狀態。在此範例中,切換星星是一個獨立的動作,不會影響父 widget 或 UI 的其餘部分,因此 widget 可以內部處理其狀態。
在管理狀態中深入了解 widget 和狀態的分離,以及狀態的管理方式。
步驟 2:子類別 StatefulWidget
#FavoriteWidget
類別管理自己的狀態,因此它會覆寫 createState()
來建立 State
物件。當架構想要建構 widget 時,會呼叫 createState()
。在此範例中,createState()
會傳回 _FavoriteWidgetState
的執行個體,您將在下一個步驟中實作它。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
步驟 3:子類別 State
#_FavoriteWidgetState
類別會儲存可以在 widget 的生命週期中變更的可變資料。當應用程式首次啟動時,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'),
),
),
],
);
}
// ···
}
_toggleFavorite()
方法會在按下 IconButton
時呼叫,並呼叫 setState()
。呼叫 setState()
至關重要,因為這會告知架構 widget 的狀態已變更,並且應該重新繪製 widget。setState()
的函式引數會在這兩個狀態之間切換 UI
star
圖示和數字 41star_border
圖示和數字 40
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
步驟 4:將狀態型 widget 插入 widget 樹狀結構中
#在應用程式的 build()
方法中,將您的自訂狀態型 widget 新增至 widget 樹狀結構。首先,找到建立 Icon
和 Text
的程式碼,然後將其刪除。在相同的位置,建立狀態型 widget
child: Row(
children: [
// ...
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
const FavoriteWidget(),
],
),
就是這樣!當您熱重新載入應用程式時,星星圖示現在應該會回應點擊事件。
遇到問題嗎?
#如果您無法執行您的程式碼,請在 IDE 中尋找可能的錯誤。偵錯 Flutter 應用程式可能有幫助。如果您仍然找不到問題,請根據 GitHub 上的互動式湖泊範例檢查您的程式碼。
如果您仍然有疑問,請參考任何一個開發人員社群頻道。
本頁的其餘部分涵蓋了管理 widget 狀態的幾種方式,並列出了其他可用的互動式 widget。
管理狀態
#誰管理狀態型 widget 的狀態?widget 本身?父 widget?兩者?另一個物件?答案是...視情況而定。有多種有效的方法可以讓您的 widget 具有互動性。身為 widget 設計師,您會根據預期 widget 的使用方式來做出決定。以下是管理狀態最常見的方法
您如何決定使用哪種方法?以下原則應該可以幫助您做出決定
如果問題中的狀態是使用者資料,例如核取方塊的勾選或取消勾選模式,或滑桿的位置,那麼最好由父 widget 管理該狀態。
如果問題中的狀態是美學,例如動畫,那麼最好由 widget 本身管理該狀態。
如果有疑問,請先在父 widget 中管理狀態。
我們將透過建立三個簡單的範例來提供管理狀態的不同方式的範例:TapboxA、TapboxB 和 TapboxC。這些範例的運作方式都類似,每個範例都會建立一個容器,當點擊時,會在綠色或灰色方塊之間切換。_active
布林值決定顏色:綠色表示啟用,灰色表示停用。
這些範例使用 GestureDetector
來擷取 Container
上的活動。
widget 管理自己的狀態
#有時候,讓 widget 在內部管理其狀態最有意義。例如,當內容超過轉譯方塊時,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,
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(),
),
),
);
}
}
父 widget 管理 widget 的狀態
#通常,由父層 Widget 管理狀態並告知子層 Widget 何時更新是最合理的做法。例如,IconButton
允許您將圖示視為可點擊的按鈕。IconButton
是一個無狀態的 Widget,因為我們決定父層 Widget 需要知道按鈕是否被點擊,以便它可以採取適當的行動。
在以下範例中,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),
),
),
),
);
}
}
混合搭配方法
#對於某些 Widget,混合搭配的方法最合理。在這種情況下,有狀態的 Widget 管理一部分狀態,而父層 Widget 管理其他方面的狀態。
在 TapboxC
範例中,在點擊按下時,方塊周圍會出現深綠色邊框。在點擊釋放時,邊框消失,方塊的顏色會改變。TapboxC
將其 _active
狀態導出到其父層,但內部管理其 _highlight
狀態。此範例有兩個 State
物件,_ParentWidgetState
和 _TapboxCState
。
_ParentWidgetState
物件
- 管理
_active
狀態。 - 實作
_handleTapboxChanged()
方法,當方塊被點擊時會呼叫此方法。 - 當發生點擊且
_active
狀態變更時,呼叫setState()
來更新 UI。
_TapboxCState
物件
- 管理
_highlight
狀態。 GestureDetector
監聽所有點擊事件。當使用者點擊按下時,它會新增高亮效果(以深綠色邊框實作)。當使用者釋放點擊時,它會移除高亮效果。- 在點擊按下、點擊釋放或點擊取消時,以及
_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,
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)),
),
),
);
}
}
另一種實作方式可能會將高亮狀態導出到父層,同時保持 active 狀態在內部,但如果您要求某人使用該點擊方塊,他們可能會抱怨這不太合理。開發人員關心方塊是否處於 active 狀態。開發人員可能不關心高亮效果是如何管理的,並且偏好由點擊方塊處理這些細節。
其他互動式 widget
#Flutter 提供了各種按鈕和類似的互動式 Widget。這些 Widget 大多數都實作了 Material Design 指南,該指南定義了一組具有主觀 UI 的元件。
如果您願意,可以使用 GestureDetector
在任何自訂 Widget 中建構互動性。您可以在 管理狀態 中找到 GestureDetector
的範例。在 處理點擊 中了解更多關於 GestureDetector
的資訊,這是 Flutter 食譜 中的一個範例。
當您需要互動性時,使用預製的 Widget 最容易。以下是一個部分清單
標準 widget
#Material Components
#Checkbox
DropdownButton
TextButton
FloatingActionButton
IconButton
Radio
ElevatedButton
Slider
Switch
TextField
資源
#以下資源可能會在為您的應用程式新增互動性時有所幫助。
手勢,Flutter 食譜 中的一個章節。
- 處理手勢
- 如何建立按鈕並使其回應輸入。
- Flutter 中的手勢
- Flutter 手勢機制的說明。
- Flutter API 文件
- 所有 Flutter 程式庫的參考文件。
- Wonderous 應用程式 運行中的應用程式、儲存庫
- 具有自訂設計和引人入勝互動的 Flutter 展示應用程式。
- Flutter 的分層設計 (影片)
- 此影片包含有關狀態和無狀態 Widget 的資訊。由 Google 工程師 Ian Hickson 呈現。
除非另有說明,否則本網站上的文件反映 Flutter 的最新穩定版本。頁面最後更新於 2024-08-16。 檢視來源 或 回報問題。