本文適合:剛入門 Flutter 想搞懂 TextField 的同學,以及已經在項目中使用,但總覺得“只會 60%”的同學。
文章會從基礎用法一直講到 Controller、Focus、表單驗證、輸入限制、常見坑,配完整代碼。
一、為什麼要系統學 TextField?
在實際業務裏,輸入框幾乎無處不在:
- 登錄 / 註冊:手機號、驗證碼、密碼
- 搜索:搜索框 + 清空按鈕 + 聯想
- 表單:收貨地址、個人信息、反饋意見
- 評論 / 聊天:多行輸入 + 發送按鈕
TextField 是 Flutter 中最基礎的輸入組件,但它涉及:
- 狀態管理(
TextEditingController) - 焦點管理(
FocusNode) - 裝飾樣式(
InputDecoration+Theme) - 驗證與表單(
TextFormField+Form) - 輸入限制(
inputFormatters) - 鍵盤行為與收起
如果這些你只是“零碎知道一點”,那這篇可以幫你把腦子裏的碎片拼成完整的知識圖。
二、TextField 是什麼?最小可用示例
1. TextField 是誰?
TextField:最基礎的輸入組件TextFormField:在Form中使用的輸入組件,天生支持表單驗證(本質也是包了一層TextField)
最簡單的 TextField:
class SimpleTextFieldDemo extends StatelessWidget {
const SimpleTextFieldDemo({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(16),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: '用户名',
hintText: '請輸入用户名',
),
),
);
}
}
關鍵點:
- 不指定
controller,內部也會維護一個 decoration負責“長什麼樣”- 這是開發中最常見的寫法之一
三、TextField 核心屬性總覽(按類別理解)
把屬性按“功能”記,比一個個死記要舒服得多。
1. 文本內容相關
TextField(
controller: _controller, // 文本控制器(推薦)
onChanged: (value) {}, // 每次內容變化回調
onSubmitted: (value) {}, // 點擊鍵盤“完成/回車”時
)
controller:讀寫文本的入口
- 讀:
_controller.text - 寫:
_controller.text = 'hello'
onChanged:每次字符變化都會觸發(帶中文輸入法時,會比較頻繁)
onSubmitted:用户點擊鍵盤上的 done / send / search 時觸發
注意:
TextField沒有initialValue屬性(那是TextFormField的),想設置默認文本就寫在controller.text裏。
2. 光標和焦點相關
TextField(
focusNode: _focusNode, // 控制焦點
autofocus: true, // 自動獲取焦點
enabled: true, // 是否可編輯
readOnly: false, // 可獲取焦點但不能改內容
showCursor: true, // 是否顯示光標
cursorColor: Colors.blue, // 光標顏色
)
enabled = false:灰掉 + 不可編輯 + 不可獲取焦點readOnly = true:可以獲取焦點、彈鍵盤(可控制),但內容不能改。很適合“點擊輸入框跳到新頁面填寫”的場景(比如“選擇地址”)
3. 鍵盤、輸入行為相關
TextField(
keyboardType: TextInputType.number, // 鍵盤類型:數字、email、多行…
textInputAction: TextInputAction.done,// 鍵盤右下角按鈕類型
maxLength: 11, // 最大長度
maxLines: 1, // 最大行數
minLines: 1, // 最小行數
inputFormatters: [ // 輸入限制
FilteringTextInputFormatter.digitsOnly,
],
)
常見鍵盤類型:
TextInputType.text:普通文本TextInputType.number:數字TextInputType.phone:手機號TextInputType.emailAddress:郵箱TextInputType.multiline:支持換行
常見 textInputAction:
TextInputAction.done:完成TextInputAction.next:下一項(比如切換到下一個輸入框)TextInputAction.search:搜索TextInputAction.send:發送
4. 文本樣式相關
TextField(
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
),
textAlign: TextAlign.left, // 對齊方式
obscureText: true, // 是否密文(密碼)
obscuringCharacter: '•', // 密文替換字符
maxLines: 1, // 單行
)
密碼框的最簡單寫法:
TextField(
obscureText: true,
decoration: const InputDecoration(
labelText: '密碼',
),
)
5. 裝飾樣式 InputDecoration(重點中的重點)
decoration 決定了 TextField 外觀的 80%。
TextField(
decoration: InputDecoration(
labelText: '用户名', // 上飄的標籤
hintText: '請輸入用户名', // 灰色提示
helperText: '用户名長度 4~16', // 底部輔助文案
errorText: null, // 錯誤提示
prefixIcon: const Icon(Icons.person), // 左側圖標
suffixIcon: IconButton( // 右側圖標
icon: const Icon(Icons.clear),
onPressed: () {},
),
border: const OutlineInputBorder(), // 默認邊框
focusedBorder: OutlineInputBorder( // 聚焦邊框
borderSide: BorderSide(color: Colors.blue),
),
),
)
常見用法:
- 登錄 / 搜索:
prefixIcon - 清空按鈕 / 顯示密碼:
suffixIcon - 錯誤提示:
errorText: '手機號格式錯誤'
四、TextEditingController:輸入框的“數據大腦”
1. 基本用法
class ControllerDemo extends StatefulWidget {
const ControllerDemo({super.key});
@override
State<ControllerDemo> createState() => _ControllerDemoState();
}
class _ControllerDemoState extends State<ControllerDemo> {
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.text = '初始文本'; // 設置默認內容
_controller.addListener(() {
debugPrint('當前內容:${_controller.text}');
});
}
@override
void dispose() {
_controller.dispose(); // 一定要釋放
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
),
ElevatedButton(
onPressed: () {
debugPrint('提交內容:${_controller.text}');
},
child: const Text('打印內容'),
),
],
);
}
}
要點:
State裏創建final TextEditingControllerinitState中初始化 / 監聽dispose裏記得dispose(),避免內存泄漏
2. 控制光標位置 & 選中內容(進階)
有時候你需要在中間插入文本、或者選中一段文本:
_controller.value = _controller.value.copyWith(
text: '新的內容',
selection: TextSelection.collapsed(offset: '新的內容'.length),
);
常見場景:
- 處理輸入格式(比如自動插入空格)
- 恢復光標位置
五、FocusNode:掌控焦點和鍵盤
1. FocusNode 的基本用法
class FocusDemo extends StatefulWidget {
const FocusDemo({super.key});
@override
State<FocusDemo> createState() => _FocusDemoState();
}
class _FocusDemoState extends State<FocusDemo> {
final FocusNode _focusNode1 = FocusNode();
final FocusNode _focusNode2 = FocusNode();
@override
void dispose() {
_focusNode1.dispose();
_focusNode2.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _focusNode1,
decoration: const InputDecoration(labelText: '輸入框1'),
),
TextField(
focusNode: _focusNode2,
decoration: const InputDecoration(labelText: '輸入框2'),
),
ElevatedButton(
onPressed: () {
FocusScope.of(context).requestFocus(_focusNode2); // 切換到輸入框2
},
child: const Text('切換到輸入框2'),
),
],
);
}
}
2. 收起鍵盤(全局通用寫法)
void hideKeyboard(BuildContext context) {
FocusScope.of(context).unfocus();
}
常見用法:
- 頁面點擊空白處收起鍵盤
- 提交後收起鍵盤
例子(外層包 GestureDetector):
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
behavior: HitTestBehavior.translucent,
child: Scaffold(
// ...
),
)
六、TextField vs TextFormField:什麼時候用誰?
1. TextFormField 是誰?
- 使用場景:需要表單驗證時
- 搭配
Form+GlobalKey<FormState>使用 - 多個
TextFormField可以統一validate()和save()
2. 登錄表單示例(手機號 + 密碼)
class LoginFormDemo extends StatefulWidget {
const LoginFormDemo({super.key});
@override
State<LoginFormDemo> createState() => _LoginFormDemoState();
}
class _LoginFormDemoState extends State<LoginFormDemo> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String _phone = '';
String _password = '';
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '手機號',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入手機號';
}
if (value.length != 11) {
return '手機號長度必須為 11 位';
}
return null;
},
onSaved: (value) => _phone = value ?? '',
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: '密碼',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入密碼';
}
if (value.length < 6) {
return '密碼至少 6 位';
}
return null;
},
onSaved: (value) => _password = value ?? '',
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() == true) {
_formKey.currentState?.save();
debugPrint('手機號=$_phone, 密碼=$_password');
}
},
child: const Text('登錄'),
),
],
),
),
);
}
}
總結:
- 不需要複雜驗證 → 用
TextField就夠了 - 多個輸入框 + 統一驗證 / 提交 → 推薦
TextFormField + Form
七、輸入限制與格式化:inputFormatters 實戰
inputFormatters 是一個 List<TextInputFormatter>,可以對每次輸入做截斷 / 替換。
1. 只允許數字輸入
TextField(
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
)
2. 小數(最多兩位小數)
class DecimalTextInputFormatter extends TextInputFormatter {
final int decimalRange;
DecimalTextInputFormatter({required this.decimalRange})
: assert(decimalRange >= 0);
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
String text = newValue.text;
if (text.isEmpty) return newValue;
// 非法字符
if (!RegExp(r'^\d*\.?\d*$').hasMatch(text)) {
return oldValue;
}
// 限制小數位
if (text.contains('.') &&
text.split('.').length == 2 &&
text.split('.').last.length > decimalRange) {
return oldValue;
}
return newValue;
}
}
使用:
TextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
DecimalTextInputFormatter(decimalRange: 2),
],
)
3. 手機號中間自動插空格(進階)
例如:138 0013 8000
思路:在 inputFormatter 中插入空格,並且恢復光標位置(略複雜,這裏就不展開光標修正邏輯)。
八、三個常用“業務輸入框”完整示例
1. 密碼框 + 顯示/隱藏眼睛按鈕
class PasswordField extends StatefulWidget {
final TextEditingController controller;
const PasswordField({super.key, required this.controller});
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscure = true;
@override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
obscureText: _obscure,
decoration: InputDecoration(
labelText: '密碼',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_obscure = !_obscure;
});
},
),
),
);
}
}
2. 搜索框:帶搜索圖標 + 清空按鈕 + 鍵盤搜索
class SearchBar extends StatefulWidget {
const SearchBar({super.key});
@override
State<SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onSearch(String keyword) {
debugPrint('搜索:$keyword');
// TODO: 調用搜索接口
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
textInputAction: TextInputAction.search,
onSubmitted: _onSearch,
decoration: InputDecoration(
hintText: '搜索內容',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isEmpty
? null
: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
setState(() {});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
filled: true,
),
onChanged: (value) {
setState(() {}); // 刷新 suffixIcon 顯隱
},
);
}
}
3. 多行評論輸入框 + 發送按鈕
class CommentInput extends StatefulWidget {
const CommentInput({super.key});
@override
State<CommentInput> createState() => _CommentInputState();
}
class _CommentInputState extends State<CommentInput> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _send() {
final text = _controller.text.trim();
if (text.isEmpty) return;
debugPrint('發送評論:$text');
_controller.clear();
setState(() {});
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: _controller,
minLines: 1,
maxLines: 4,
decoration: const InputDecoration(
hintText: '寫下你的評論...',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
onPressed: _controller.text.trim().isEmpty ? null : _send,
),
],
);
}
}
九、和鍵盤的交互:完成、下一項、收起
1. textInputAction + onEditingComplete
TextField(
textInputAction: TextInputAction.next,
onEditingComplete: () {
// 比如切到下一個 TextField
FocusScope.of(context).nextFocus();
},
)
常見組合:
- 表單第一項:
TextInputAction.next+nextFocus() - 最後一項:
TextInputAction.done+unfocus()+ 提交表單
2. 手動收起鍵盤的幾種方式
1)最推薦:unfocus
FocusScope.of(context).unfocus();
2)極端情況:調用系統通道隱藏
SystemChannels.textInput.invokeMethod('TextInput.hide');
一般用不到,有 unfocus() 足夠了。
十、國際化 / 中文輸入法的一點注意
- 中文輸入法有“拼寫中狀態”(組合輸入,尚未上屏)
onChanged在拼寫過程中會觸發多次- 一些比較激進的
inputFormatter會在組合狀態下干擾輸入
如果你對輸入有複雜控制(比如在 onChanged 中強行改 controller.text),要注意不要破壞 IME 的組合狀態,否則會出現“中文打不出來”“光標亂跳”等問題。
實在複雜的場景,建議單獨開一個 demo 專門調試各種輸入法(包括 iOS / Android)。
十一、TextField 常見坑總結
1. 在 build() 裏 new TextEditingController
❌ 錯誤寫法:
@override
Widget build(BuildContext context) {
final controller = TextEditingController(); // 每次 build 都 new
return TextField(controller: controller);
}
這樣會導致:
- 每次重建都重新 new controller,內容丟失
- 還可能有內存泄漏
✅ 正確寫法:
- 在
State中聲明為成員變量,並在dispose()裏釋放
2. 忘記 dispose() controller / focusNode
- TextField 內部會訂閲 controller 的變化
- 如果不
dispose,頁面關掉後仍然有監聽 → 內存泄漏
3. TextField 外層沒有高度約束
例如直接寫在 Column 裏且沒有 Expanded / 父佈局約束,可能會報錯:
RenderFlex children have non-zero flex but incoming height constraints are unbounded
解決方式:
- 給外層加
SizedBox/Expanded/Container(height: ...) - 或者正確使用
Column+mainAxisSize
4. 鍵盤遮擋輸入框
常見場景:頁面底部的輸入框被鍵盤擋住了。
解決方向:
- Scaffold 上:
resizeToAvoidBottomInset: true(默認一般就是 true) - 外層使用可滾動佈局,比如
SingleChildScrollView - 或者使用更高級的庫:
flutter_keyboard_visibility、keyboard_actions等
5. 濫用 onChanged 做重型操作
onChanged觸發頻率非常高(每個字符輸入 / 刪除)- 不要在裏面做複雜同步操作(比如每改一次就請求網絡)
- 必須做的話,建議加防抖(debounce)