Flutter 實作常見的輸入框欄位
前言
今天介紹一下 Flutter 怎麼實作出我們常見的輸入框欄位以及欄位的驗證。
因為我也是初次學習 Flutter,如果文章中有些說明的不正確的地方,還請大家不吝嗇留言,大家一起多多進步,謝謝。
簡單介紹一下,Flutter 是一個跨平台的框架,使用 Dart 程式語言,最大的特色是撰寫一份程式碼可以多個平台應用,包含 iOS 系統、Android 系統、Web 網頁、Desktop 桌面。
安裝方式大家可以上網查詢有相當多的資源,也可以參考本站的另一篇優質文章 → Flutter 跨平台開發架構-安裝與開發工具</a >
我使用的是 Visual Studio Code IDE 、Xcode 的 iPhone 15 Pro - iOS 17.5 模擬器。
Flutter 介紹
Flutter 專案建置起來後,會看到很多層層疊疊的檔案與資料夾真是眼花撩亂,別擔心主要撰寫程式碼的地方在 lib 資料夾內的 main.dart 檔案。
當然不是所有的程式碼一定都要寫在 main.dart 中,也可以根據自己的需求建立 components、pages 或 assets 等來調整架構。
初始執行畫面畫面:
先移除 body 的內容與 floatingActionButton Widget 等預設的初始程式:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: const Center(),
);
}
}
使用輸入框 TextField
首先用 Center Widget 將所有內容都置中,輸入框 Widget 使用的是 TextField。
宣告變數 nameController, nameController, 是一個新的 TextEditingController 控制器實例;TextEditingController 主要用於控制和監聽 TextField的輸入和內容也可以用來取得、設置 TextField 的值。
還需要將 nameController 傳遞給 TextField 的 controller 屬性中,這樣我們就可以通過 nameController 來管理和監聽 TextField 的輸入。
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController nameController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: TextField(
controller: nameController,
keyboardType: TextInputType.none,
decoration: const InputDecoration(
labelText: "Name",
hintText: "請輸入用戶名稱",
prefixIcon: Icon(Icons.person),
),
),
),
);
}
}
輸出畫面如下:
如何取得 TextField 的值?
我們要將 TextField 取出的值顯示在畫面上,並且有一個觸發的按鈕,所以畫面中需要再增加 Text 跟 Button,先前的 Center Widget 不支援多個 Widget,所以將 Center 改成表示由上往下的列 Column Widget,並且指定mainAxisAlignment 屬性為置中,保持所有的 Widget 在畫面的中心位置。
宣告字串 _nameText 變數來存放由 TextField 取出的值,並且在 Text Widget 使用了字符串插值 $,將 _nameText 變數的值嵌入到字符串中,當 _nameText 的值變化時,顯示的文本會自動更新以反映新的值。
觸發機制當使用者按下送出按鈕時,藉由 nameController.text 取得 TextField Widget 的值,保存至 _nameText 變數,按鈕使用 ElevatedButton Widget,必須帶入 child 與 onPressed 屬性,onPressed 屬性顧名思義就是按下時的動作,在這裡,綁定了我們新建立的 _sumbit 方法。在 _sumbit 中使用了 setState 方法, setState 是通知 Flutter 框架狀態已經改變,應該重建 Widget 樹中的這個 State。
注意,TextEditingController 是一個有狀態的物件,即使沒有使用的監聽器 nameController.addListener(),也會在內部進行狀態管理和資源分配。因此,為了避免內存洩漏和確保應用程序的資源有效利用,應該在不再需要該控制器時清除資源,可以在 Widget 被銷毀時調用 nameController.dispose() 方法來釋放資源。
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _nameController = TextEditingController();
String? _nameText;
void _submit() {
setState(() {
_nameText = _nameController.text;
});
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _nameController,
keyboardType: TextInputType.none,
decoration: const InputDecoration(
isCollapsed: false,
labelText: "Name",
hintText: "請輸入用戶名稱",
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: const Text('送出'),
),
Text('Name Field: $_nameText'),
],
),
);
}
}
輸出畫面如下:
不想要藉由按鈕來觸發,想直接利用監聽器的方式自動觸發,可參考以下程式碼:
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController nameController = TextEditingController();
String? _nameText;
void initState() {
super.initState();
// 監聽器
nameController.addListener(() {
setState(() {
_nameText = nameController.text;
});
});
}
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 略...
);
}
}
輸入框的欄位驗證:
有輸入框欄位就少不了欄位的驗證,我們已經知道了 TextField Widget,那實際上我們欄位如果需有要檢核條件的情況下會使用 TextFormField Widget。
TextFormField 是 TextField 的一個擴展,TextFormField 結合了 TextField 和 Form,可以方便地進行表單驗證,所以通常用於需要表單驗證的情境。如果要顯示錯誤訊息的話需要搭配 Form Widget 一起使用。
因為我們需要訪問和操控這個 Form 的狀態,所以創建了一個與FormState 關聯的 GlobalKey,並且命名為 _formKey,將 _formKey 傳遞給 Form Widget 的 key 屬性,從而使這個 Form Widget 與 _formKey 關聯起來,這樣我們就可以使用 _formKey 來訪問和操控 Form 的狀態,例如驗證表單欄位。
至於欄位的部分,在此添加了兩個 TextFormField:
- 第一個是 Email 欄位,希望檢核條件有必填以及必須有效的電子郵件格式。
- 第二個是 Password 欄位,希望檢核條件有必填以及必密碼長度需大於 5 位。
欄位的檢核邏輯需寫在TextFormField 的 validator 屬性中,其中 Email 欄位有點小特別的地方是採用 EmailValidator 驗證。EmailValidator 是 Dart 中的一個庫,專門用於驗證電子郵件地址的格式,使用時需要在 pubspec.yaml 文件中添加 email_validator 依賴,並且在開發的 Dart 檔案中導入 email_validator</span >。
pubspec.yaml
dependencies:
flutter:
sdk: flutter
email_validator: ^2.0.1
main.dart
import 'package:email_validator/email_validator.dart';
在點擊送出按鈕後會觸發 _submit 方法,在 _submit 方法中,我們使用 _formKey.currentState!.validate() 來驗證表單中的所有藍位,validate() 方法會調用每個表單欄位的 validator 函數,如果所有字段都通過驗證,則返回 true,否則返回 false。我們藉由返回不同的狀態來顯示不同的文字。
題外話,因為不想要欄位貼邊的關係,所以我在 Form 外層加上了 Padding Widget 讓所有方向(上、下、左、右)都添加 16 DIP 的內邊距,看起來輸入框欄位就不會跟螢幕邊連起來。
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
String? _text;
void _submit() {
if (_formKey.currentState!.validate()) {
String email = _emailController.text;
String password = _passwordController.text;
setState(() {
_text = '${_nameController.text}\nEmail: $email\nPassword: $password';
});
} else {
setState(() {
_text = '${_nameController.text}\nOOPS!!!!! Form is InValidate.';
});
}
}
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {
_text = _nameController.text;
});
});
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextField(
controller: _nameController,
keyboardType: TextInputType.none,
decoration: const InputDecoration(
isCollapsed: false,
labelText: "Name",
hintText: "請輸入用戶名稱",
prefixIcon: Icon(Icons.person),
),
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: "Email"),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入電子信箱';
} else if (!EmailValidator.validate(value)) {
return '請輸入正確的信箱格式';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入密碼';
} else if (value.length <= 5) {
return '密碼長度需大於 5 位';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: const Text('送出'),
),
Text('Name: $_text'),
],
),
),
),
],
),
);
}
}
輸出畫面如下:
欄位輸入錯誤時,畫面如下:
感謝大家看到最後 ♥