Flutter

Flutter 實作常見的輸入框欄位

Aimer 2024/06/26 10:23:49
587

前言

今天介紹一下 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'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

輸出畫面如下:

欄位輸入錯誤時,畫面如下:

感謝大家看到最後 ♥

Aimer