Forms & Input Validation trong Flutter

1. Tổng quan về Forms & Input Validation

Forms & Input Validation là hai thành phần quan trọng trong quá trình phát triển ứng dụng Flutter, được sử dụng để thu thập thông tin từ người dùng và kiểm tra tính hợp lệ của dữ liệu trước khi xử lý hoặc gửi lên máy chủ.

Trong các ứng dụng thực tế, biểu mẫu xuất hiện ở nhiều chức năng như đăng ký tài khoản, đăng nhập, cập nhật hồ sơ cá nhân, đặt hàng, thanh toán, tìm kiếm và nhập thông tin sản phẩm. Những chức năng này đều yêu cầu người dùng cung cấp dữ liệu theo một cấu trúc nhất định.

Forms là biểu mẫu dùng để tổ chức các trường nhập liệu và những thành phần tương tác nhằm thu thập dữ liệu từ người dùng. Trong Flutter, biểu mẫu thường được xây dựng bằng các widget như Form, TextFormField, TextField, DropdownButtonFormField, Checkbox, Radio, Switch và các thành phần giao diện liên quan.

Input Validation là quá trình kiểm tra dữ liệu đầu vào nhằm xác định dữ liệu có đáp ứng các điều kiện đã quy định hay không. Quá trình này giúp phát hiện các trường hợp nhập thiếu thông tin, sai định dạng, vượt quá giới hạn hoặc không phù hợp với yêu cầu của ứng dụng.

Ví dụ, trong biểu mẫu đăng ký tài khoản, hệ thống có thể yêu cầu tên người dùng không được để trống, email phải đúng định dạng, mật khẩu có độ dài tối thiểu và mật khẩu xác nhận phải trùng khớp với mật khẩu ban đầu.

Khi kết hợp Forms và Input Validation, ứng dụng có thể cung cấp trải nghiệm nhập liệu rõ ràng, hạn chế lỗi dữ liệu và giảm các thao tác không cần thiết trong quá trình xử lý thông tin.

2. Vai trò của Forms trong Flutter

Forms đóng vai trò trung tâm trong những chức năng yêu cầu người dùng nhập hoặc lựa chọn dữ liệu. Thay vì xây dựng từng trường nhập liệu một cách độc lập, Flutter cho phép nhóm nhiều trường vào một biểu mẫu để quản lý và kiểm tra dữ liệu theo một quy trình thống nhất.

Các vai trò chính của Forms bao gồm:

Thu thập dữ liệu có cấu trúc: Form tập hợp các trường nhập liệu liên quan thành một nhóm, giúp dữ liệu được tổ chức rõ ràng và thuận tiện cho việc xử lý.

Quản lý trạng thái nhập liệu: Các trường trong biểu mẫu có thể thay đổi theo thao tác của người dùng. Flutter cung cấp nhiều cơ chế để theo dõi nội dung hiện tại, trạng thái hợp lệ và các lựa chọn đã được thực hiện.

Hỗ trợ kiểm tra dữ liệu: Form có thể thực hiện kiểm tra nhiều trường cùng lúc thông qua phương thức validate(), từ đó xác định liệu biểu mẫu có đáp ứng đầy đủ các quy tắc hay không.

Hiển thị thông báo lỗi: Khi dữ liệu không hợp lệ, ứng dụng có thể hiển thị thông báo ngay bên dưới trường nhập liệu tương ứng để người dùng nhận biết và sửa lỗi.

Điều phối thao tác gửi dữ liệu: Form hỗ trợ xây dựng quy trình gửi biểu mẫu, trong đó dữ liệu chỉ được xử lý tiếp khi đáp ứng các điều kiện kiểm tra ở phía giao diện.

Nâng cao trải nghiệm người dùng: Một biểu mẫu được thiết kế tốt giúp người dùng hiểu rõ thông tin cần nhập, định dạng mong đợi và cách khắc phục khi xảy ra lỗi.

3. Các widget quan trọng trong Forms

3.1. Form

Form là widget dùng để nhóm và quản lý nhiều trường nhập liệu có khả năng kiểm tra dữ liệu. Đây là thành phần nền tảng khi xây dựng các biểu mẫu phức tạp trong Flutter.

Form thường được kết hợp với GlobalKey<FormState> để truy cập trạng thái của biểu mẫu từ những vị trí khác trong cây widget. Thông qua đối tượng FormState, ứng dụng có thể thực hiện kiểm tra dữ liệu, đặt lại biểu mẫu hoặc lưu trạng thái nhập liệu.

Các phương thức thường được sử dụng gồm:

Phương thức Chức năng
validate() Chạy các hàm kiểm tra dữ liệu của những trường hỗ trợ validation và trả về kết quả hợp lệ của biểu mẫu.
save() Gọi hàm onSaved của các trường trong Form để lưu dữ liệu theo cách được lập trình.
reset() Đặt lại trạng thái của các trường về giá trị ban đầu.

Ví dụ khai báo Form:

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            _formKey.currentState!.save();
          }
        },
        child: const Text('Submit'),
      ),
    ],
  ),
)

Trong ví dụ trên, _formKey được sử dụng để truy cập trạng thái của Form. Khi người dùng nhấn nút Submit, phương thức validate() kiểm tra các trường có cung cấp quy tắc validation. Nếu tất cả đều hợp lệ, phương thức save() được gọi để thực hiện các hàm onSaved tương ứng.

Lưu ý rằng Form không tự động lưu dữ liệu vào cơ sở dữ liệu hoặc gửi dữ liệu đến API. Việc lưu trữ và gửi dữ liệu cần được triển khai riêng.

3.2. TextFormField

forms - Flutter Textformfield validator Focuses Last TextFormfield on validation error instead of first - Stack Overflow

TextFormField là widget chuyên dùng để nhập văn bản trong một Form. Widget này kết hợp khả năng nhập liệu của TextField với cơ chế validation và lưu dữ liệu của FormField.

TextFormField thường được sử dụng cho các trường như họ tên, email, số điện thoại, mật khẩu, địa chỉ và nội dung mô tả.

Một số thuộc tính quan trọng:

Thuộc tính Chức năng
controller Quản lý và truy xuất nội dung văn bản hiện tại.
validator Xác định quy tắc kiểm tra nội dung nhập vào.
onSaved Xử lý giá trị khi Form gọi phương thức save().
decoration Tùy chỉnh giao diện, nhãn, gợi ý và đường viền.
keyboardType Xác định loại bàn phím phù hợp với dữ liệu cần nhập.
obscureText Ẩn nội dung văn bản, thường dùng cho mật khẩu.
maxLength Giới hạn số lượng ký tự được nhập.
autovalidateMode Xác định thời điểm tự động chạy validation.
inputFormatters Kiểm soát hoặc biến đổi dữ liệu trong quá trình nhập.
textInputAction Xác định hành động của phím trên bàn phím, chẳng hạn Next hoặc Done.

Ví dụ về trường nhập email:

TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email',
    hintText: '[email protected]',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Vui lòng nhập email';
    }

    if (!value.contains('@')) {
      return 'Email không hợp lệ';
    }

    return null;
  },
)

Hàm validator nhận giá trị hiện tại của trường nhập liệu. Nếu dữ liệu không đáp ứng yêu cầu, hàm trả về một chuỗi thông báo lỗi. Nếu dữ liệu hợp lệ, hàm trả về null.

Trong ứng dụng thực tế, kiểm tra bằng ký tự @ chỉ là ví dụ đơn giản. Việc xác thực email có thể cần quy tắc chặt chẽ hơn, đồng thời phải xác minh email thực sự tồn tại thông qua quy trình xác nhận của hệ thống nếu chức năng yêu cầu.

3.3. TextField

TextField là widget cho phép người dùng nhập và chỉnh sửa văn bản. Widget này phù hợp với những trường nhập liệu độc lập không cần tích hợp trực tiếp vào cơ chế validation của Form.

TextField thường được sử dụng trong thanh tìm kiếm, hộp nhập tin nhắn, bộ lọc dữ liệu và những trường nhập liệu đơn giản.

Ví dụ:

final searchController = TextEditingController();

TextField(
  controller: searchController,
  decoration: const InputDecoration(
    hintText: 'Tìm kiếm sản phẩm',
    prefixIcon: Icon(Icons.search),
  ),
  onChanged: (value) {
    print(value);
  },
)

Thuộc tính onChanged được gọi khi nội dung nhập liệu thay đổi, cho phép ứng dụng cập nhật kết quả tìm kiếm hoặc trạng thái giao diện theo thời gian thực.

Điểm khác biệt quan trọng là TextField không cung cấp thuộc tính validator như TextFormField. Khi cần sử dụng cơ chế validation tiêu chuẩn của Form, nên ưu tiên TextFormField.

3.4. GlobalKey và FormState

GlobalKey<FormState> là công cụ cho phép truy cập trạng thái của một Form cụ thể từ bên ngoài widget Form đó.

Thông thường, GlobalKey được khai báo bên trong một StatefulWidget:

final _formKey = GlobalKey<FormState>();

Sau đó, key được gắn vào Form:

Form(
  key: _formKey,
  child: const Column(
    children: [
      // Các trường nhập liệu
    ],
  ),
)

Khi cần kiểm tra dữ liệu, ứng dụng truy cập trạng thái của Form:

if (_formKey.currentState!.validate()) {
  // Tiếp tục xử lý dữ liệu
}

Dấu ! cho biết lập trình viên khẳng định currentState không phải null. Trong một số tình huống, nên kiểm tra giá trị này trước khi sử dụng để tránh lỗi khi trạng thái chưa được khởi tạo.

GlobalKey cần được tạo một lần trong vòng đời của widget, không nên khai báo lại trong mỗi lần gọi build(), vì điều đó có thể làm mất tính ổn định của key và gây ra những vấn đề không cần thiết trong quá trình quản lý trạng thái.

4. Các thành phần nhập liệu phổ biến

Ngoài trường nhập văn bản, Flutter còn hỗ trợ nhiều widget khác nhau để xây dựng biểu mẫu phù hợp với từng loại dữ liệu.

4.1. DropdownButtonFormField

DropdownButtonFormField cho phép người dùng chọn một giá trị từ danh sách có sẵn. Widget này phù hợp với những trường có số lượng lựa chọn xác định như giới tính, tỉnh thành, danh mục sản phẩm hoặc vai trò tài khoản.

Ví dụ:

String? selectedRole;

DropdownButtonFormField<String>(
  value: selectedRole,
  decoration: const InputDecoration(
    labelText: 'Vai trò',
    border: OutlineInputBorder(),
  ),
  items: const [
    DropdownMenuItem(
      value: 'user',
      child: Text('Người dùng'),
    ),
    DropdownMenuItem(
      value: 'admin',
      child: Text('Quản trị viên'),
    ),
  ],
  onChanged: (value) {
    selectedRole = value;
  },
  validator: (value) {
    if (value == null) {
      return 'Vui lòng chọn vai trò';
    }
    return null;
  },
)

Đoạn mã minh họa cách tạo danh sách lựa chọn và kiểm tra người dùng đã chọn giá trị hay chưa. Với phiên bản Flutter mới, cần sử dụng đúng API hiện hành của DropdownButtonFormField, bao gồm initialValue hoặc value tùy phiên bản SDK.

Trong các hệ thống phân quyền thực tế, không được dựa vào giá trị vai trò do người dùng tự chọn ở giao diện để cấp quyền quản trị. Quyền truy cập phải được xác minh ở phía máy chủ.

4.2. Checkbox

Checkbox cho phép người dùng bật hoặc tắt một lựa chọn độc lập. Widget này thường được sử dụng cho các tùy chọn như đồng ý điều khoản sử dụng, ghi nhớ đăng nhập hoặc nhận thông báo.

Ví dụ:

bool acceptedTerms = false;

CheckboxListTile(
  title: const Text('Tôi đồng ý với điều khoản'),
  value: acceptedTerms,
  onChanged: (value) {
    setState(() {
      acceptedTerms = value ?? false;
    });
  },
)

Checkbox không trực tiếp cung cấp cơ chế validator theo cách của TextFormField. Khi muốn đưa lựa chọn này vào quy trình kiểm tra Form, có thể sử dụng FormField<bool> hoặc kiểm tra biến trạng thái trước khi cho phép gửi biểu mẫu.

4.3. Radio

Radio cho phép người dùng chọn một giá trị trong một nhóm các lựa chọn loại trừ lẫn nhau. Ví dụ, người dùng có thể chọn một trong các phương thức nhận hàng hoặc một trong các hình thức thanh toán.

Khi xây dựng giao diện, nên quản lý giá trị lựa chọn bằng một biến trạng thái duy nhất. Trong những phiên bản Flutter hiện đại, có thể sử dụng RadioGroup để quản lý nhóm radio theo API tương ứng của SDK.

4.4. Switch

Switch là widget dùng để bật hoặc tắt một thiết lập. Thành phần này thường xuất hiện trong phần cài đặt tài khoản, chẳng hạn như bật thông báo đẩy, bật chế độ tối hoặc cho phép nhận email quảng cáo.

Switch phù hợp với các lựa chọn mang tính cấu hình hơn là dữ liệu văn bản. Nếu trạng thái của Switch là điều kiện bắt buộc, ứng dụng cần tự kiểm tra trạng thái đó hoặc tích hợp nó vào một FormField thích hợp.

4.5. DatePicker và các trường chọn ngày

DatePicker cho phép người dùng lựa chọn ngày tháng thông qua giao diện lịch. Thành phần này thường được dùng để nhập ngày sinh, ngày giao hàng hoặc thời điểm bắt đầu và kết thúc một hoạt động.

Flutter cung cấp hàm showDatePicker() để hiển thị hộp thoại chọn ngày. Giá trị trả về thường là Future<DateTime?>, vì người dùng có thể chọn ngày hoặc đóng hộp thoại mà không lựa chọn.

Ứng dụng cần kiểm tra các điều kiện như ngày được chọn không nằm trong quá khứ, ngày kết thúc không sớm hơn ngày bắt đầu hoặc độ tuổi đáp ứng yêu cầu nghiệp vụ.

5. Tổng quan về Input Validation

Input Validation là quá trình xác minh dữ liệu đầu vào trước khi dữ liệu được chấp nhận hoặc chuyển sang bước xử lý tiếp theo.

Trong ứng dụng Flutter, validation thường được thực hiện ở giao diện người dùng nhằm cung cấp phản hồi nhanh. Tuy nhiên, kiểm tra phía giao diện không thể thay thế việc xác thực dữ liệu ở backend, bởi người dùng có thể gửi yêu cầu trực tiếp đến API mà không thông qua giao diện Flutter.

Một hệ thống validation tốt cần đảm bảo ba mục tiêu chính.

Thứ nhất, dữ liệu phải đáp ứng các yêu cầu về định dạng và kiểu dữ liệu. Ví dụ, trường tuổi phải là số nguyên hợp lệ, email phải đúng cấu trúc được chấp nhận và ngày tháng phải được chuyển đổi thành giá trị hợp lệ.

Thứ hai, dữ liệu phải tuân thủ các giới hạn được quy định. Ví dụ, mật khẩu phải có độ dài tối thiểu, số lượng sản phẩm không được âm và trường mô tả không được vượt quá số ký tự cho phép.

Thứ ba, dữ liệu phải đáp ứng các quy tắc nghiệp vụ. Ví dụ, mã giảm giá phải còn hiệu lực, số lượng đặt hàng phải nằm trong giới hạn tồn kho và tài khoản phải có quyền thực hiện thao tác tương ứng.

6. Các loại Input Validation

6.1. Required Validation

Required Validation là kiểm tra nhằm đảm bảo trường dữ liệu bắt buộc không bị bỏ trống.

Đây là một trong những quy tắc phổ biến nhất trong các biểu mẫu đăng ký, đăng nhập, đặt hàng và cập nhật thông tin cá nhân.

Ví dụ:

validator: (value) {
  if (value == null || value.trim().isEmpty) {
    return 'Trường này không được để trống';
  }

  return null;
}

Phương thức trim() loại bỏ khoảng trắng ở đầu và cuối chuỗi. Nhờ đó, trường hợp người dùng chỉ nhập các dấu cách cũng được xem là chưa cung cấp nội dung hợp lệ.

Tuy nhiên, không phải mọi trường đều cần bắt buộc nhập. Những trường tùy chọn nên được phân biệt rõ ràng để tránh yêu cầu người dùng cung cấp thông tin không cần thiết.

6.2. Format Validation

Format Validation kiểm tra dữ liệu có tuân theo định dạng được quy định hay không.

Các ví dụ phổ biến gồm:

  • Email có cấu trúc hợp lệ.

  • Số điện thoại đáp ứng quy tắc về số lượng chữ số và mã vùng.

  • Ngày tháng có thể chuyển đổi thành ngày hợp lệ.

  • Mã bưu chính đáp ứng định dạng của khu vực tương ứng.

  • Mã sản phẩm tuân thủ quy ước đặt tên của hệ thống.

Ví dụ kiểm tra email ở mức cơ bản:

bool isValidEmail(String email) {
  final pattern =
      RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');

  return pattern.hasMatch(email);
}

Hàm này sử dụng Regular Expression (RegExp) để kiểm tra cấu trúc email thông dụng. Tuy nhiên, biểu thức trên không đảm bảo địa chỉ email thực sự tồn tại hoặc có thể nhận thư. Nếu hệ thống cần xác minh quyền sở hữu email, cần triển khai thêm bước gửi mã xác nhận hoặc liên kết xác thực.

6.3. Length Validation

Length Validation kiểm tra độ dài của dữ liệu nhập vào.

Ví dụ, mật khẩu có thể cần tối thiểu 8 ký tự, tên người dùng không được vượt quá 50 ký tự và phần mô tả sản phẩm có thể giới hạn ở 500 ký tự.

Ví dụ:

validator: (value) {
  if (value == null || value.length < 8) {
    return 'Mật khẩu phải có ít nhất 8 ký tự';
  }

  return null;
}

Trong những trường hợp cần kiểm tra cả độ dài tối thiểu và tối đa, nên xây dựng điều kiện rõ ràng để tránh dữ liệu quá ngắn hoặc quá dài.

Cần lưu ý rằng giới hạn độ dài mật khẩu không nên là tiêu chí duy nhất để đánh giá độ an toàn. Hệ thống còn cần bảo vệ quá trình truyền dữ liệu, lưu trữ mật khẩu bằng thuật toán băm phù hợp và có cơ chế chống tấn công đăng nhập.

6.4. Numeric Validation

Numeric Validation kiểm tra dữ liệu đầu vào có phải số hợp lệ và nằm trong giới hạn được chấp nhận hay không.

Ví dụ, ứng dụng bán hàng cần đảm bảo số lượng sản phẩm là số nguyên dương, giá sản phẩm không âm và số tiền thanh toán phù hợp với quy tắc nghiệp vụ.

validator: (value) {
  if (value == null || value.trim().isEmpty) {
    return 'Vui lòng nhập số lượng';
  }

  final quantity = int.tryParse(value);

  if (quantity == null || quantity <= 0) {
    return 'Số lượng phải là số nguyên dương';
  }

  return null;
}

int.tryParse() chuyển chuỗi thành số nguyên nếu dữ liệu phù hợp. Khi chuyển đổi thất bại, phương thức trả về null thay vì phát sinh lỗi chuyển đổi như một số phương thức khác.

Đối với giá tiền hoặc số thập phân, cần sử dụng kiểu dữ liệu và cách biểu diễn phù hợp để hạn chế sai số. Trong hệ thống thanh toán, việc biểu diễn tiền bằng đơn vị nhỏ nhất như đồng hoặc cent dưới dạng số nguyên thường an toàn hơn so với dựa hoàn toàn vào số thực dấu phẩy động.

6.5. Matching Validation

Matching Validation kiểm tra sự tương ứng giữa hai hoặc nhiều trường dữ liệu.

Trường hợp điển hình là xác nhận mật khẩu. Người dùng nhập mật khẩu hai lần để giảm khả năng sai sót khi đăng ký hoặc thay đổi mật khẩu.

final passwordController = TextEditingController();

TextFormField(
  controller: passwordController,
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Mật khẩu',
  ),
)

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Xác nhận mật khẩu',
  ),
  validator: (value) {
    if (value != passwordController.text) {
      return 'Mật khẩu xác nhận không khớp';
    }

    return null;
  },
)

Trong ví dụ trên, trường xác nhận mật khẩu so sánh nội dung nhập vào với giá trị hiện tại của trường mật khẩu. Để hoàn thiện biểu mẫu, cần kiểm tra trường xác nhận không được để trống và giải phóng TextEditingController bằng dispose() khi widget bị hủy.

6.6. Business Rule Validation

Business Rule Validation là kiểm tra dữ liệu theo các quy tắc nghiệp vụ của ứng dụng.

Khác với việc kiểm tra định dạng, validation nghiệp vụ tập trung vào việc xác định dữ liệu có phù hợp với trạng thái và quy định của hệ thống hay không.

Ví dụ:

  • Ngày kết thúc phải sau ngày bắt đầu.

  • Số lượng sản phẩm đặt mua không được vượt quá số lượng tồn kho.

  • Mã khuyến mãi phải nằm trong thời gian sử dụng.

  • Tài khoản phải đủ điều kiện để thực hiện giao dịch.

  • Số tiền thanh toán phải khớp với đơn hàng.

  • Người dùng phải đáp ứng điều kiện độ tuổi của một dịch vụ.

Những quy tắc phụ thuộc vào dữ liệu mới nhất của hệ thống cần được kiểm tra ở backend. Ví dụ, tồn kho có thể thay đổi sau khi người dùng mở biểu mẫu, vì vậy kiểm tra tồn kho chỉ ở giao diện là chưa đủ.

7. Quy trình Validation trong Flutter

Quá trình kiểm tra dữ liệu trong Flutter thường được triển khai theo một trình tự nhất định.

1. Người dùng nhập dữ liệu

Nhập văn bản, chọn ngày hoặc lựa chọn giá trị

2. Thu thập và kiểm tra dữ liệu

Chạy validator và kiểm tra các điều kiện

Dữ liệu không hợp lệ

Hiển thị lỗi và yêu cầu chỉnh sửa

Dữ liệu hợp lệ

Tiếp tục xử lý hoặc gửi API

Trong Flutter, phương thức validate() của Form sẽ gọi các hàm validator của những trường có hỗ trợ validation. Nếu có trường trả về thông báo lỗi, Form được xem là không hợp lệ. Khi tất cả các trường đều vượt qua validation, ứng dụng có thể tiếp tục xử lý dữ liệu.

Tuy nhiên, kết quả hợp lệ ở giao diện chỉ xác nhận dữ liệu đáp ứng các quy tắc đã được triển khai ở phía client. Backend vẫn phải thực hiện xác thực và kiểm tra quyền truy cập trước khi chấp nhận yêu cầu.

8. Các phương pháp hiển thị lỗi

8.1. Validation khi gửi biểu mẫu

Đây là phương pháp chỉ thực hiện kiểm tra khi người dùng nhấn nút gửi dữ liệu.

Ưu điểm của phương pháp này là dễ triển khai, giảm việc hiển thị thông báo lỗi khi người dùng chưa hoàn thành việc nhập liệu và phù hợp với những biểu mẫu ngắn.

Nhược điểm là người dùng có thể phải sửa nhiều trường cùng lúc nếu biểu mẫu có nhiều điều kiện kiểm tra.

8.2. AutovalidateMode

Flutter cung cấp AutovalidateMode để điều khiển thời điểm tự động chạy validation.

Các giá trị thường gặp:

Giá trị Ý nghĩa
AutovalidateMode.disabled Không tự động kiểm tra theo cơ chế autovalidation.
AutovalidateMode.always Tự động kiểm tra theo các lần cập nhật phù hợp của Form hoặc trường nhập liệu.
AutovalidateMode.onUserInteraction Bắt đầu tự động kiểm tra sau khi người dùng tương tác với trường.
AutovalidateMode.onUnfocus Kiểm tra khi trường mất focus; cần kiểm tra hỗ trợ theo phiên bản Flutter đang sử dụng.

Ví dụ:

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.trim().isEmpty) {
            return 'Vui lòng nhập họ tên';
          }

          return null;
        },
      ),
    ],
  ),
)

Cách này giúp thông báo lỗi xuất hiện khi người dùng bắt đầu tương tác với biểu mẫu, thay vì chờ đến lúc gửi dữ liệu. Tuy nhiên, việc lựa chọn chế độ validation cần phù hợp với trải nghiệm mong muốn, tránh khiến giao diện liên tục xuất hiện lỗi trong khi người dùng đang nhập.

8.3. Input Formatter

Formatted Number Text Field Widget in Flutter (for Credit Cards and Phone Numbers) | by Adrish Shahid | Medium

Input Formatter kiểm soát nội dung ngay trong quá trình người dùng nhập liệu. Thành phần này khác với validator ở chỗ formatter có thể ngăn hoặc biến đổi dữ liệu trước khi nó được chấp nhận vào trường nhập liệu.

Flutter cung cấp TextInputFormatter cùng những formatter phổ biến như:

  • FilteringTextInputFormatter.digitsOnly: chỉ cho phép chữ số.

  • LengthLimitingTextInputFormatter: giới hạn số ký tự.

  • FilteringTextInputFormatter.allow(...): cho phép các ký tự phù hợp với quy tắc được xác định.

  • FilteringTextInputFormatter.deny(...): loại bỏ các ký tự được chỉ định.

Ví dụ:

TextFormField(
  keyboardType: TextInputType.number,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(10),
  ],
  decoration: const InputDecoration(
    labelText: 'Số điện thoại',
  ),
)

Trong ví dụ này, trường nhập liệu chỉ cho phép chữ số và giới hạn độ dài tối đa 10 ký tự.

Tuy nhiên, Input Formatter không thay thế validation. Dữ liệu vẫn cần được kiểm tra để đảm bảo đáp ứng yêu cầu nghiệp vụ, đồng thời backend phải xác thực lại dữ liệu nhận được từ client.

9. Xây dựng biểu mẫu đăng ký tài khoản hoàn chỉnh

Ví dụ dưới đây minh họa một biểu mẫu đăng ký tài khoản gồm họ tên, email, mật khẩu và xác nhận mật khẩu. Biểu mẫu áp dụng các quy tắc kiểm tra cơ bản, thông báo lỗi và xử lý dữ liệu sau khi validation thành công.

import 'package:flutter/material.dart';

class RegisterPage extends StatefulWidget {
  const RegisterPage({super.key});

  @override
  State<RegisterPage> createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  final _formKey = GlobalKey<FormState>();

  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();

  bool _isPasswordVisible = false;
  bool _isConfirmPasswordVisible = false;

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    super.dispose();
  }

  String? _validateName(String? value) {
    if (value == null || value.trim().isEmpty) {
      return 'Vui lòng nhập họ tên';
    }

    if (value.trim().length < 2) {
      return 'Họ tên phải có ít nhất 2 ký tự';
    }

    return null;
  }

  String? _validateEmail(String? value) {
    if (value == null || value.trim().isEmpty) {
      return 'Vui lòng nhập email';
    }

    final emailPattern =
        RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');

    if (!emailPattern.hasMatch(value.trim())) {
      return 'Email không đúng định dạng';
    }

    return null;
  }

  String? _validatePassword(String? value) {
    if (value == null || value.isEmpty) {
      return 'Vui lòng nhập mật khẩu';
    }

    if (value.length < 8) {
      return 'Mật khẩu phải có ít nhất 8 ký tự';
    }

    return null;
  }

  String? _validateConfirmPassword(String? value) {
    if (value == null || value.isEmpty) {
      return 'Vui lòng xác nhận mật khẩu';
    }

    if (value != _passwordController.text) {
      return 'Mật khẩu xác nhận không khớp';
    }

    return null;
  }

  void _submitForm() {
    FocusScope.of(context).unfocus();

    if (!_formKey.currentState!.validate()) {
      return;
    }

    final name = _nameController.text.trim();
    final email = _emailController.text.trim();

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Dữ liệu hợp lệ cho tài khoản $email'),
      ),
    );

    debugPrint('Name: $name');
    debugPrint('Email: $email');

    // Gửi dữ liệu đến API đăng ký tại đây.
    // Không in mật khẩu ra log trong ứng dụng thực tế.
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Đăng ký tài khoản'),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Form(
            key: _formKey,
            autovalidateMode:
                AutovalidateMode.onUserInteraction,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                TextFormField(
                  controller: _nameController,
                  keyboardType: TextInputType.name,
                  textInputAction: TextInputAction.next,
                  decoration: const InputDecoration(
                    labelText: 'Họ và tên',
                    prefixIcon: Icon(Icons.person_outline),
                    border: OutlineInputBorder(),
                  ),
                  validator: _validateName,
                ),
                const SizedBox(height: 16),

                TextFormField(
                  controller: _emailController,
                  keyboardType: TextInputType.emailAddress,
                  textInputAction: TextInputAction.next,
                  decoration: const InputDecoration(
                    labelText: 'Email',
                    prefixIcon: Icon(Icons.email_outlined),
                    border: OutlineInputBorder(),
                  ),
                  validator: _validateEmail,
                ),
                const SizedBox(height: 16),

                TextFormField(
                  controller: _passwordController,
                  obscureText: !_isPasswordVisible,
                  textInputAction: TextInputAction.next,
                  decoration: InputDecoration(
                    labelText: 'Mật khẩu',
                    prefixIcon: const Icon(Icons.lock_outline),
                    border: const OutlineInputBorder(),
                    suffixIcon: IconButton(
                      icon: Icon(
                        _isPasswordVisible
                            ? Icons.visibility_off
                            : Icons.visibility,
                      ),
                      onPressed: () {
                        setState(() {
                          _isPasswordVisible =
                              !_isPasswordVisible;
                        });
                      },
                    ),
                  ),
                  validator: _validatePassword,
                ),
                const SizedBox(height: 16),

                TextFormField(
                  controller: _confirmPasswordController,
                  obscureText: !_isConfirmPasswordVisible,
                  textInputAction: TextInputAction.done,
                  decoration: InputDecoration(
                    labelText: 'Xác nhận mật khẩu',
                    prefixIcon: const Icon(Icons.lock_outline),
                    border: const OutlineInputBorder(),
                    suffixIcon: IconButton(
                      icon: Icon(
                        _isConfirmPasswordVisible
                            ? Icons.visibility_off
                            : Icons.visibility,
                      ),
                      onPressed: () {
                        setState(() {
                          _isConfirmPasswordVisible =
                              !_isConfirmPasswordVisible;
                        });
                      },
                    ),
                  ),
                  validator: _validateConfirmPassword,
                  onFieldSubmitted: (_) => _submitForm(),
                ),
                const SizedBox(height: 24),

                ElevatedButton(
                  onPressed: _submitForm,
                  child: const Padding(
                    padding: EdgeInsets.symmetric(vertical: 14),
                    child: Text('Đăng ký'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Phân tích các thành phần trong ví dụ

Quản lý trạng thái Form: _formKey liên kết với Form, cho phép gọi validate() để kiểm tra toàn bộ dữ liệu trước khi xử lý.

Quản lý nội dung nhập liệu: Các TextEditingController cho phép truy cập nội dung của từng trường, đồng thời được giải phóng trong dispose() để quản lý tài nguyên đúng cách.

Tách biệt logic validation: Mỗi trường có một hàm kiểm tra riêng. Cách tổ chức này giúp mã nguồn dễ đọc, thuận tiện sửa đổi và có thể tái sử dụng.

Ẩn và hiện mật khẩu: Thuộc tính obscureText kết hợp với trạng thái Boolean cho phép người dùng xem hoặc ẩn nội dung mật khẩu khi cần.

Kiểm tra trước khi xử lý: Phương thức _submitForm() chỉ tiếp tục khi validate() trả về true. Nếu dữ liệu không hợp lệ, các thông báo lỗi tương ứng sẽ được hiển thị.

Tối ưu trải nghiệm nhập liệu: SingleChildScrollView giúp biểu mẫu có thể cuộn khi bàn phím xuất hiện hoặc khi màn hình thiết bị có chiều cao hạn chế.

Đây là ví dụ minh họa cho validation phía giao diện. Để hoàn thiện chức năng đăng ký, cần bổ sung API backend, xử lý lỗi từ máy chủ, trạng thái đang gửi, chống gửi lặp và các cơ chế bảo mật cần thiết.

10. Nguyên tắc thiết kế Forms & Input Validation

10.1. Kiểm tra dữ liệu ở đúng thời điểm

Validation cần được triển khai theo cách giúp người dùng phát hiện lỗi nhanh chóng nhưng không làm gián đoạn quá trình nhập liệu.

Đối với biểu mẫu ngắn, có thể kiểm tra khi người dùng nhấn nút gửi. Với biểu mẫu dài hoặc những trường cần phản hồi tức thời, có thể áp dụng validation khi tương tác hoặc khi trường mất focus.

Các thông báo lỗi không nên xuất hiện quá sớm khi người dùng chưa có cơ hội nhập dữ liệu. Đồng thời, lỗi đã được khắc phục nên được cập nhật phù hợp để người dùng không nhầm lẫn rằng dữ liệu vẫn không hợp lệ.

10.2. Thông báo lỗi rõ ràng và cụ thể

Thông báo lỗi cần cho người dùng biết trường nào có vấn đề và cách sửa lỗi.

Ví dụ, thông báo “Dữ liệu không hợp lệ” cung cấp ít thông tin hơn so với “Mật khẩu phải có ít nhất 8 ký tự”.

Thông báo nên ngắn gọn, sử dụng ngôn ngữ dễ hiểu và phù hợp với ngữ cảnh của trường nhập liệu. Đối với các ứng dụng đa ngôn ngữ, nội dung lỗi nên được quản lý thông qua hệ thống localization thay vì viết cố định trực tiếp trong mã nguồn.

10.3. Không chỉ dựa vào màu sắc để báo lỗi

Thông báo lỗi không nên chỉ thể hiện bằng cách đổi màu đường viền của trường nhập liệu. Ứng dụng cần cung cấp thêm văn bản giải thích, biểu tượng hoặc những dấu hiệu trực quan khác để người dùng dễ nhận biết.

Điều này đặc biệt quan trọng đối với người có hạn chế về thị giác màu sắc hoặc sử dụng thiết bị với độ tương phản thấp.

10.4. Hạn chế dữ liệu không cần thiết

Biểu mẫu chỉ nên yêu cầu những thông tin cần thiết cho chức năng hiện tại. Việc yêu cầu quá nhiều trường có thể làm giảm tỷ lệ hoàn thành biểu mẫu và tạo ra trải nghiệm sử dụng không thuận tiện.

Các trường tùy chọn cần được phân biệt với trường bắt buộc. Những dữ liệu nhạy cảm cũng cần được thu thập, truyền tải và lưu trữ theo các yêu cầu bảo mật phù hợp.

10.5. Tách biệt giao diện và logic nghiệp vụ

Đối với ứng dụng nhỏ, các hàm validator có thể được viết trực tiếp trong widget. Tuy nhiên, khi ứng dụng phát triển, nên tách logic validation thành các hàm tiện ích, lớp kiểm tra dữ liệu hoặc các thành phần riêng theo kiến trúc dự án.

Cách tổ chức này giúp giảm sự phụ thuộc giữa giao diện và nghiệp vụ, đồng thời hỗ trợ kiểm thử tự động và tái sử dụng logic ở nhiều màn hình.

10.6. Xác thực lại ở backend

Đây là nguyên tắc quan trọng nhất trong việc bảo vệ tính toàn vẹn của dữ liệu.

Validation ở Flutter giúp người dùng nhận phản hồi nhanh, nhưng không thể đảm bảo dữ liệu gửi lên máy chủ là an toàn. Người dùng có thể sửa ứng dụng, gọi API trực tiếp hoặc bỏ qua toàn bộ kiểm tra phía client.

Vì vậy, backend cần kiểm tra lại kiểu dữ liệu, giới hạn giá trị, quy tắc nghiệp vụ, quyền truy cập và các điều kiện bảo mật trước khi xử lý yêu cầu.

10.7. Đảm bảo khả năng truy cập

Biểu mẫu cần hỗ trợ nhiều cách tương tác khác nhau, bao gồm bàn phím, thao tác chạm và các công nghệ hỗ trợ. Nhãn trường nhập liệu phải rõ ràng, thứ tự focus hợp lý và thông báo lỗi cần có khả năng được nhận biết thông qua các cơ chế hỗ trợ tiếp cận phù hợp.

Đối với các biểu mẫu dài, có thể sử dụng textInputAction để hỗ trợ chuyển từ trường này sang trường khác. Những trường bắt buộc và tùy chọn cũng nên được trình bày rõ ràng để giảm nhầm lẫn.

11. Các lỗi thường gặp khi triển khai Forms

Lỗi Nguyên nhân và cách khắc phục
Không hiển thị thông báo lỗi Chưa khai báo validator, chưa gọi validate() hoặc trường nhập liệu chưa được tích hợp vào Form phù hợp.
Không lấy được dữ liệu Chưa sử dụng TextEditingController, onSaved hoặc cơ chế quản lý trạng thái phù hợp.
Lỗi khi truy cập currentState Form chưa được gắn key đúng cách hoặc trạng thái chưa sẵn sàng. Có thể kiểm tra currentState trước khi sử dụng.
Dữ liệu chỉ có khoảng trắng vẫn được chấp nhận Chưa sử dụng trim() khi kiểm tra trường bắt buộc.
Mật khẩu xác nhận không cập nhật lỗi Chưa thiết kế lại quá trình validation khi mật khẩu gốc thay đổi.
Bàn phím che mất biểu mẫu Chưa bố trí giao diện cuộn hoặc khoảng trống phù hợp với kích thước màn hình.
Controller không được giải phóng Quên gọi dispose() khi sử dụng TextEditingController trong StatefulWidget.
Dữ liệu không hợp lệ vẫn được lưu Chưa kiểm tra kết quả validate() trước khi tiếp tục xử lý.
Dữ liệu hợp lệ ở client nhưng bị backend từ chối Quy tắc validation giữa client và server khác nhau hoặc dữ liệu vi phạm điều kiện nghiệp vụ ở thời điểm gửi yêu cầu.
Người dùng nhấn gửi nhiều lần Chưa quản lý trạng thái đang gửi và chưa ngăn các yêu cầu trùng lặp khi cần thiết.

12. Kiểm thử Forms & Input Validation

Kiểm thử là bước quan trọng để đảm bảo biểu mẫu hoạt động chính xác trong nhiều tình huống khác nhau. Một biểu mẫu không chỉ cần hoạt động khi dữ liệu hợp lệ mà còn phải xử lý đúng các trường hợp dữ liệu thiếu, sai định dạng hoặc vượt quá giới hạn.

Các trường hợp kiểm thử nên bao gồm:

Kiểm thử dữ liệu hợp lệ: Nhập đầy đủ thông tin đúng định dạng và xác nhận biểu mẫu cho phép tiếp tục xử lý.

Kiểm thử trường bắt buộc: Để trống một hoặc nhiều trường và kiểm tra thông báo lỗi xuất hiện đúng vị trí.

Kiểm thử dữ liệu biên: Nhập giá trị ở giới hạn tối thiểu, tối đa và những giá trị ngay bên ngoài giới hạn.

Kiểm thử định dạng: Nhập email sai cấu trúc, số điện thoại không hợp lệ hoặc dữ liệu không thể chuyển đổi thành kiểu mong muốn.

Kiểm thử sự tương ứng giữa các trường: Nhập mật khẩu và xác nhận mật khẩu khác nhau, sau đó kiểm tra kết quả validation.

Kiểm thử giao diện: Kiểm tra biểu mẫu trên các kích thước màn hình khác nhau, khi bàn phím mở và khi người dùng sử dụng chế độ chữ lớn.

Kiểm thử lỗi API: Kiểm tra trường hợp máy chủ từ chối dữ liệu, mất kết nối mạng hoặc trả về lỗi nghiệp vụ.

Flutter hỗ trợ kiểm thử widget thông qua flutter_test, cho phép mô phỏng thao tác nhập liệu, nhấn nút và kiểm tra nội dung hiển thị. Ngoài ra, có thể sử dụng unit test để kiểm tra các hàm validation độc lập mà không cần khởi chạy toàn bộ giao diện.

13. Tổng kết

Forms & Input Validation là nền tảng quan trọng trong phát triển ứng dụng Flutter có khả năng tiếp nhận và xử lý dữ liệu người dùng. Forms giúp tổ chức các trường nhập liệu thành một cấu trúc thống nhất, trong khi Input Validation đảm bảo dữ liệu đáp ứng các yêu cầu về định dạng, giới hạn và quy tắc nghiệp vụ trước khi được xử lý.

Các thành phần cốt lõi gồm Form, TextFormField, TextField, GlobalKey<FormState>, validator, TextEditingController và TextInputFormatter. Việc hiểu rõ vai trò của từng thành phần giúp lập trình viên xây dựng biểu mẫu dễ bảo trì, kiểm tra dữ liệu hiệu quả và hạn chế lỗi trong quá trình sử dụng.

Khi phát triển ứng dụng thực tế, cần kết hợp validation hợp lý, thông báo lỗi rõ ràng, quản lý trạng thái chính xác, thiết kế giao diện dễ sử dụng và kiểm tra dữ liệu ở cả client lẫn backend. Đây là những nguyên tắc quan trọng để xây dựng các chức năng đăng nhập, đăng ký, đặt hàng, thanh toán và quản lý dữ liệu đáng tin cậy.

Bài viết khác

Flutter Layout & Responsive UI

      Trong phát triển ứng dụng hiện đại, giao diện người dùng không chỉ cần đẹp mà còn phải thích ứng tốt với nhiều kích thước màn hình, tỷ lệ khung hình và phương thức tương tác khác nhau. Một ứng dụng có thể được sử dụng trên điện thoại nhỏ, máy tính […]

StatelessWidget và StatefulWidget trong Flutter

    Trong số các loại widget phổ biến nhất, StatelessWidget và StatefulWidget giữ vai trò đặc biệt quan trọng. Việc phân biệt hai loại widget này không chỉ giúp lập trình viên viết mã đúng mà còn ảnh hưởng đến khả năng tổ chức, bảo trì và tối ưu hiệu năng của ứng dụng. StatelessWidget phù hợp […]

Test Case & Test Plan & Test Data

1. Lời mở đầu và vị trí nền tảng của bộ ba quản lý kiểm thử trong dự án phần mềm Trong quy trình sản xuất phần mềm hiện đại, nếu hoạt động lập trình (Coding) là việc hiện thực hóa các ý tưởng thiết kế thành sản phẩm chạy được, thì hoạt động kiểm […]

Navigation & Routing

Navigation & Routing trong Flutter 1. Navigation & Routing Navigation & Routing là cơ chế điều hướng giữa các màn hình trong ứng dụng Flutter. Cơ chế này cho phép người dùng di chuyển từ màn hình này sang màn hình khác, quay lại màn hình trước đó, truyền dữ liệu giữa các màn hình […]

Material & Cupertino

Material & Cupertino trong Flutter 1. Material Material Design là hệ thống thiết kế giao diện người dùng (UI Design System) do Google phát triển. Material cung cấp các nguyên tắc thiết kế, màu sắc, kiểu chữ, bố cục, hiệu ứng chuyển động và các thành phần giao diện nhằm giúp ứng dụng có giao […]

Flutter Widget Fundamentals

       Flutter là bộ công cụ phát triển giao diện người dùng do Google xây dựng, cho phép lập trình viên tạo ứng dụng đa nền tảng từ một cơ sở mã nguồn duy nhất. Điểm đặc biệt nhất của Flutter nằm ở triết lý mọi thứ đều là widget. Từ một dòng […]

Leave a Reply

Your email address will not be published. Required fields are marked *