處理使用者輸入
現在你已經知道如何在 Flutter 應用程式中管理狀態,那麼要如何讓使用者與你的應用互動並改變其狀態呢?
處理使用者輸入簡介
#作為一個多平台的 UI 框架,使用者有許多不同的方式可以與 Flutter 應用程式互動。本節的資源將介紹一些常見的元件(Widgets),這些元件可用於在你的應用中啟用使用者互動。
接下來,我們將介紹幾個 Material 元件(Widgets),這些元件支援在 Flutter 應用程式中處理常見的使用者輸入情境。
按鈕
#
按鈕讓使用者可以透過點擊或輕觸,在 UI 中觸發動作。Material 函式庫提供了多種按鈕類型,這些按鈕在功能上相似,但針對不同用途有不同的樣式,包括:
ElevatedButton:具有立體感的按鈕。可用於為原本較為平面的版面配置增添層次感。FilledButton:實心填色的按鈕,適合用於重要且最終的動作,例如 儲存、立即加入 或 確認。Tonal Button:介於FilledButton與OutlinedButton之間的按鈕。 適用於需要比外框按鈕更強調、但又不是最高優先順序的情境,例如 下一步。OutlinedButton:帶有文字與明顯邊框的按鈕。 這類按鈕通常用於重要但非主要動作。TextButton:可點擊的文字,沒有邊框。 由於文字按鈕沒有明顯的邊框,必須依賴其在內容中的位置來提供語境。IconButton:帶有圖示的按鈕。FloatingActionButton:懸浮於內容之上的圖示按鈕,用於強調主要動作。
通常在建構一個按鈕時會有三個主要面向:樣式、回呼(callback)以及其子元件(child),如下方 ElevatedButton 範例程式碼所示:
按鈕的回呼函式
onPressed,決定當按鈕被點擊時會發生什麼事,因此這個函式通常是你更新應用程式狀態的地方。 如果回呼設為null,則按鈕會被停用,使用者按下時不會有任何反應。按鈕的
child,即顯示於按鈕內容區域的元件,通常是文字或圖示,用來說明按鈕的用途。最後,按鈕的
style控制其外觀,例如顏色、邊框等。
int count = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
setState(() {
count += 1;
});
},
child: const Text('Enabled'),
);
}
檢查點: 完成這個教學,學習如何建立一個 「收藏」按鈕:為你的 Flutter 應用程式新增互動功能
API 文件:ElevatedButton • FilledButton • OutlinedButton • TextButton • IconButton • FloatingActionButton
文字
#有多種元件(Widgets)支援文字輸入。
SelectableText
#Flutter 的 Text 元件會在螢幕上顯示文字, 但不允許使用者反白或複製文字。 SelectableText 則會顯示一串_可由使用者選取_的文字。@override
Widget build(BuildContext context) {
return const SelectableText('''
Two households, both alike in dignity,
In fair Verona, where we lay our scene,
From ancient grudge break to new mutiny,
Where civil blood makes civil hands unclean.
From forth the fatal loins of these two foes''');
}
RichText
#RichText 可讓你在應用程式中顯示一串豐富格式的文字(rich text)。 TextSpan 類似於 RichText,可以讓你顯示具有不同文字樣式的部分文字。 它並不是用來處理使用者輸入,但如果你允許使用者編輯和格式化文字時會很有用。@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
text: 'Hello ',
style: DefaultTextStyle.of(context).style,
children: const <TextSpan>[
TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: ' world!'),
],
),
);
}
TextField
#TextField 讓使用者可以透過實體鍵盤或螢幕鍵盤,在文字方塊中輸入文字。
TextField 擁有許多不同的屬性與設定方式,以下是幾個重點:
InputDecoration決定文字欄位(text field)的外觀,例如顏色與邊框。controller:TextEditingController用來控制正在編輯的文字。為什麼你可能需要一個 controller 呢? 預設情況下,應用程式的使用者可以在文字欄位中輸入內容,但如果你希望以程式方式控制TextField並清除其值,例如,你就需要一個TextEditingController。onChanged:當使用者更改文字欄位的值(例如插入或刪除文字)時,這個 callback 函式會被觸發。onSubmitted:當使用者表示已完成該欄位的文字編輯時,這個 callback 會被觸發;例如,當文字欄位取得焦點時,點擊「enter」鍵。
這個類別還支援其他可設定的屬性,例如 obscureText,可在輸入時將每個字母顯示為 readOnly 圓點, 以及 readOnly,可防止使用者更改文字內容。final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Mascot Name',
),
);
}
檢查點:
完成這個四部曲的 cookbook 系列,帶你一步步學會如何建立文字欄位 (text field)、
取得其值,並更新你的應用程式狀態:
Form
#Form 是一個可選的容器,用於將多個
表單欄位元件(如 TextField)分組在一起。
每個獨立的表單欄位都應包裹在 FormField
元件中,並以 Form 元件作為共同的祖先。
有些便利元件已經預先將表單欄位元件包裹在
FormField 中。
例如,TextField 的 Form 元件版本就是 TextFormField。
使用 Form 可以取得 FormState,
讓你能夠儲存、重設及驗證每個從此 Form
繼承下來的 FormField。
你也可以提供 GlobalKey 來識別特定的表單,
如下方程式碼所示:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
hintText: 'Enter your email',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
// Process data.
}
},
child: const Text('Submit'),
),
),
],
),
);
}檢查點: 完成本教學以學習如何建立具有驗證功能的表單。
展示: 表單應用程式
程式碼: 表單應用程式程式碼
API 文件: TextField • RichText • SelectableText • Form
從一組選項中選擇值
#提供使用者從多個選項中選擇的方法。
SegmentedButton
#SegmentedButton 讓使用者可以從 2-5 個精簡的項目中選擇。
資料型別 <T> 可以是內建型別,例如 int、String、bool,或是 enum(列舉)。 SegmentedButton 有幾個相關屬性:
segments,一個ButtonSegment的清單,每個代表一個「區段」 或使用者可選擇的選項。 在視覺上,每個ButtonSegment可以有圖示、文字標籤,或兩者皆有。multiSelectionEnabled指示是否允許使用者 選擇多個選項。此屬性預設為 false。selected用來識別目前所選的值。 注意:selected的型別為Set<T>,因此如果只允許 使用者選擇一個值,該值必須以 僅有一個元素的Set提供。當使用者選擇任何區段時,
onSelectionChanged回呼會被觸發。 它會提供已選擇區段的清單,讓你可以更新應用程式狀態。其他樣式參數可讓你調整按鈕的外觀。 例如,
style接受ButtonStyle, 提供設定selectedIcon的方式。
enum Calendar { day, week, month, year }
// StatefulWidget...
Calendar calendarView = Calendar.day;
@override
Widget build(BuildContext context) {
return SegmentedButton<Calendar>(
segments: const <ButtonSegment<Calendar>>[
ButtonSegment<Calendar>(
value: Calendar.day,
label: Text('Day'),
icon: Icon(Icons.calendar_view_day)),
ButtonSegment<Calendar>(
value: Calendar.week,
label: Text('Week'),
icon: Icon(Icons.calendar_view_week)),
ButtonSegment<Calendar>(
value: Calendar.month,
label: Text('Month'),
icon: Icon(Icons.calendar_view_month)),
ButtonSegment<Calendar>(
value: Calendar.year,
label: Text('Year'),
icon: Icon(Icons.calendar_today)),
],
selected: <Calendar>{calendarView},
onSelectionChanged: (Set<Calendar> newSelection) {
setState(() {
Suggested change
// By default there is only a single segment that can be
// selected at one time, so its value is always the first
// By default, only a single segment can be
// selected at one time, so its value is always the first
calendarView = newSelection.first;
});
},
);
}
Chip
#Chip 是一種精簡的方式,用於在特定情境下表示屬性、文字、實體或動作。 針對不同的使用情境,還有專門的 Chip 元件(Widgets):
- InputChip 以精簡的形式,代表一個複雜的資訊片段,例如實體(人物、地點或事物),或對話文字。
- ChoiceChip 允許從一組選項中選擇一個。Choice chips 內含相關的描述文字或分類。
- FilterChip 使用標籤或描述性文字來篩選內容。
- ActionChip 代表與主要內容相關的動作。
每個 Chip 元件都需要一個 label。 它也可以選擇性地包含 avatar(例如圖示或使用者的大頭貼圖片)以及 onDeleted 回呼函式,這會顯示一個刪除圖示,當觸發時會刪除該 chip。 Chip 元件的外觀也可以透過設定多個選用參數(如 shape、color 和 iconTheme)來自訂。
你通常會使用 Wrap,這是一個能將其子元件(children)分布在多個水平或垂直行的元件,以確保你的 chips 能自動換行,不會在應用程式邊緣被截斷。@override
Widget build(BuildContext context) {
return const SizedBox(
width: 500,
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 4,
children: [
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_chef.png')),
label: Text('Chef Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage:
AssetImage('assets/images/dash_firefighter.png')),
label: Text('Firefighter Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_musician.png')),
label: Text('Musician Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_artist.png')),
label: Text('Artist Dash'),
),
],
),
);
}
DropdownMenu
#DropdownMenu 讓使用者可以從選單中選擇一個選項,並將所選的文字放入 TextField 中。 它同時允許使用者根據文字輸入來篩選選單項目。
可設定的參數包括以下內容:
dropdownMenuEntries提供一個DropdownMenuEntry清單, 用於描述每個選單項目。 選單可能包含如文字標籤,以及前置或後置圖示等資訊。 (這也是唯一必填的參數。)TextEditingController允許以程式方式控制TextField。- 當使用者選擇某個選項時,
onSelected回呼函式會被觸發。 initialSelection允許你設定預設值。- 另外還有其他參數可用於 自訂元件(Widget)的外觀與行為。
enum ColorLabel {
blue('Blue', Colors.blue),
pink('Pink', Colors.pink),
green('Green', Colors.green),
orange('Orange', Colors.orange),
grey('Grey', Colors.grey);
const ColorLabel(this.label, this.color);
final String label;
final Color color;
}
// StatefulWidget...
@override
Widget build(BuildContext context) {
return DropdownMenu<ColorLabel>(
initialSelection: ColorLabel.green,
controller: colorController,
// requestFocusOnTap is enabled/disabled by platforms when it is null.
// On mobile platforms, this is false by default. Setting this to true will
// trigger focus request on the text field and virtual keyboard will appear
// afterward. On desktop platforms however, this defaults to true.
requestFocusOnTap: true,
label: const Text('Color'),
onSelected: (ColorLabel? color) {
setState(() {
selectedColor = color;
});
},
dropdownMenuEntries: ColorLabel.values
.map<DropdownMenuEntry<ColorLabel>>(
(ColorLabel color) {
return DropdownMenuEntry<ColorLabel>(
value: color,
label: color.label,
enabled: color.label != 'Grey',
style: MenuItemButton.styleFrom(
foregroundColor: color.color,
),
);
}).toList(),
);
}
Slider
#Slider 元件讓使用者可以透過移動指示器來調整數值, 例如音量條。
Slider 元件的設定參數:
value代表目前的滑桿數值onChanged是當滑桿被移動時會觸發的回呼函式min和max設定滑桿允許的最小與最大值divisions設定使用者可沿著軌道移動滑桿把手的離散間隔
double _currentVolume = 1;
@override
Widget build(BuildContext context) {
return Slider(
value: _currentVolume,
max: 5,
divisions: 5,
label: _currentVolume.toString(),
onChanged: (double value) {
setState(() {
_currentVolume = value;
});
},
);
}
影片:
Slider, RangeSlider, CupertinoSlider(本週元件 Widget of the Week)
API 文件: SegmentedButton • DropdownMenu • Slider • Chip
在多個值之間切換
#你的 UI 可以透過多種方式實現值的切換。
Checkbox、Switch 與 Radio
#提供一個選項,讓單一值可以開啟或關閉。
這三種元件(Widgets)在功能邏輯上是一致的,
因為它們都建立於 ToggleableStateMixin 之上,但
每一種在呈現方式上略有不同:
Checkbox是一個容器,當為 false 時為空,
當為 true 時則會顯示勾選符號。Switch有一個把手,當為 false 時在左側,
當為 true 時會滑到右側。Radio類似於Checkbox,都是一個容器,
當為 false 時為空,當為 true 時則會被填滿。
Checkbox 與 Switch 的設定包含:
- 一個
value,其值為true或false - 以及一個
onChanged回呼(callback),當
使用者切換元件時會觸發
Checkbox
#bool isChecked = false;
@override
Widget build(BuildContext context) {
return Checkbox(
checkColor: Colors.white,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
);
}
Switch
#bool light = true;
@override
Widget build(BuildContext context) {
return Switch(
// This bool value toggles the switch.
value: light,
activeColor: Colors.red,
onChanged: (bool value) {
// This is called when the user toggles the switch.
setState(() {
light = value;
});
},
);
}
Radio
#一組Radio按鈕,讓使用者在互斥的值之間進行選擇。當使用者在同一組中選取某個 radio 按鈕時,其他 radio 按鈕會自動取消選取。
- 某個特定
Radio按鈕的value代表該按鈕的值, - 一組 radio 按鈕中被選取的值,會由
groupValue參數來識別。 Radio也有onChanged回呼函式(callback),當使用者點擊時會觸發,這點與Switch和Checkbox類似
enum Character { musician, chef, firefighter, artist }
class RadioExample extends StatefulWidget {
const RadioExample({super.key});
@override
State<RadioExample> createState() => _RadioExampleState();
}
class _RadioExampleState extends State<RadioExample> {
Character? _character = Character.musician;
void setCharacter(Character? value) {
setState(() {
_character = value;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
title: const Text('Musician'),
leading: Radio<Character>(
value: Character.musician,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Chef'),
leading: Radio<Character>(
value: Character.chef,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Firefighter'),
leading: Radio<Character>(
value: Character.firefighter,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Artist'),
leading: Radio<Character>(
value: Character.artist,
groupValue: _character,
onChanged: setCharacter,
),
),
],
);
}
}
加碼:CheckboxListTile 與 SwitchListTile
#這些便利元件(Widgets)與一般的 checkbox 和 switch 元件相同, 但額外支援標籤(作為ListTile)。double timeDilation = 1.0;
bool _lights = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
CheckboxListTile(
title: const Text('Animate Slowly'),
value: timeDilation != 1.0,
onChanged: (bool? value) {
setState(() {
timeDilation = value! ? 10.0 : 1.0;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
SwitchListTile(
title: const Text('Lights'),
value: _lights,
onChanged: (bool value) {
setState(() {
_lights = value;
});
},
secondary: const Icon(Icons.lightbulb_outline),
),
],
);
}
API 文件: Checkbox • CheckboxListTile • Switch • SwitchListTile • Radio
選擇日期或時間
#元件(Widgets)可讓使用者選擇日期與時間。
有一組對話框可讓使用者選擇日期或時間, 如以下章節所示。 除了日期型別不同外—— DateTime 用於日期,TimeOfDay 用於時間—— 這些對話框的運作方式類似,您可以透過提供以下參數來設定:
- 預設的
initialDate或initialTime - 或
initialEntryMode,用來決定顯示哪種選擇器 UI。
DatePickerDialog
#此對話框允許使用者選擇單一天或一段日期範圍。 可透過呼叫 showDatePicker 函式來啟動, 該函式會回傳 Future<DateTime>, 因此請記得要使用 await 等待這個非同步函式的結果!DateTime? selectedDate;
@override
Widget build(BuildContext context) {
var date = selectedDate;
return Column(children: [
Text(
date == null
? "You haven't picked a date yet."
: DateFormat('MM-dd-yyyy').format(date),
),
ElevatedButton.icon(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
var pickedDate = await showDatePicker(
context: context,
initialEntryMode: DatePickerEntryMode.calendarOnly,
initialDate: DateTime.now(),
firstDate: DateTime(2019),
lastDate: DateTime(2050),
);
setState(() {
selectedDate = pickedDate;
});
},
label: const Text('Pick a date'),
)
]);
}
TimePickerDialog
#TimePickerDialog 是一個用於顯示時間選擇器的對話框(Dialog)。 你可以透過呼叫 showTimePicker() 函式來啟動它。 與回傳 Future<DateTime> 不同, showTimePicker 則會回傳 Future<TimeOfDay>。 同樣地,別忘了要對這個函式呼叫使用 await!TimeOfDay? selectedTime;
@override
Widget build(BuildContext context) {
var time = selectedTime;
return Column(children: [
Text(
time == null ? "You haven't picked a time yet." : time.format(context),
),
ElevatedButton.icon(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
var pickedTime = await showTimePicker(
context: context,
initialEntryMode: TimePickerEntryMode.dial,
initialTime: TimeOfDay.now(),
);
setState(() {
selectedTime = pickedTime;
});
},
label: const Text('Pick a time'),
)
]);
}
API 文件: showDatePicker • showTimePicker
滑動與滑出
#Dismissible 是一個元件(Widget),可讓使用者透過滑動來將其移除。 它有多項可設定參數,包括:
- 一個
child元件(Widget) - 當使用者滑動時會觸發的
onDismissed回呼(callback) - 例如
background等樣式參數 - 請務必同時包含一個
key物件,以便能在元件樹中與其他同層的Dismissible元件區分開來。
List<int> items = List<int>.generate(100, (int index) => index);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (BuildContext context, int index) {
return Dismissible(
background: Container(
color: Colors.green,
),
key: ValueKey<int>(items[index]),
onDismissed: (DismissDirection direction) {
setState(() {
items.removeAt(index);
});
},
child: ListTile(
title: Text(
'Item ${items[index]}',
),
),
);
},
);
}
檢查點: 完成這個教學,學習如何使用 dismissible 元件來實作滑動以關閉。
API 文件: Dismissible
想找更多元件(Widgets)嗎?
#本頁僅介紹了幾個常用的 Material 元件(Widgets), 你可以在 Flutter 應用程式中用來處理使用者輸入(Input)。 請參考 Material Widget library 及 Material Library API 文件,取得完整的元件清單。
範例展示: 參考 Flutter 的 Material 3 Demo,其中精選了 Material 函式庫中 可用的使用者輸入元件(Widgets)範例。
如果 Material 與 Cupertino 函式庫沒有你需要的元件, 可以到 pub.dev 搜尋 Flutter 與 Dart 社群維護的套件。 例如,flutter_slidable 套件提供了 比前一節介紹的 Dismissible 元件更可自訂化的 Slidable 元件。
使用 GestureDetector 建立互動元件
#你已經翻遍元件函式庫、pub.dev、問過寫程式的朋友, 還是找不到符合你想要的使用者互動方式的元件嗎? 你可以自行建立自訂元件,並使用 GestureDetector 讓它具備互動性。
檢查點: 以這份教學為起點,建立你自己的_自訂_按鈕元件, 並能處理點擊事件。
參考資料: 參考 Taps, drags, and other gestures,瞭解如何在 Flutter 中監聽 並回應各種手勢。
加碼影片: 想知道 Flutter 的
GestureArena如何將原始的使用者互動資料 轉換成人類可辨識的點擊、拖曳與縮放等概念嗎? 請參考這部影片:GestureArena (Decoding Flutter)
別忘了無障礙設計!
#如果你正在建立自訂元件, 請使用 Semantics 元件來標註其語意。 它能為螢幕閱讀器與其他基於語意分析的工具 提供描述與中繼資料。
API 文件: GestureDetector • Semantics
測試
#當你完成應用程式的使用者互動設計後, 別忘了撰寫測試,確保一切如預期運作!
以下教學將帶你一步步撰寫模擬使用者互動的測試:
檢查點: 依照這篇 點擊、拖曳與輸入文字 教學,學習如何 使用
WidgetTester在你的應用程式中模擬並測試使用者互動。
加碼教學: 處理滾動 教學將示範如何透過元件測試 滾動列表,驗證元件清單是否包含預期內容。
下一步:網路功能
#本頁介紹了如何處理使用者輸入。 現在你已經學會如何處理來自應用程式使用者的輸入, 可以進一步讓你的應用程式更有趣——加入外部資料。 在下一節中,你將學習如何透過網路為你的應用程式取得資料、 如何進行 JSON 轉換、驗證,以及其他網路相關功能。
意見回饋
#由於本網站區塊仍在持續演進中, 我們歡迎你的意見回饋!