Một form có TextField và nút gửi chưa tự có chính sách dữ liệu hợp lệ. Nếu phần hiển thị lỗi, lấy giá trị và xử lý nút bấm nằm rải rác, bạn dễ lưu chuỗi trắng hoặc tưởng rằng save() đã tự kiểm tra mọi điều kiện.
Bài này tạo form kiểm tra tên dự án bằng Form và TextFormField. Tài liệu Flutter được đối chiếu ngày 25/10/2026; cookbook hiện ghi phạm vi Flutter 3.47. Ví dụ được rà theo API nhưng chưa build hoặc chạy widget test trong lượt biên tập. Nó chỉ kiểm tra dữ liệu local, chưa tạo dự án trên server hay lưu database.
1. Ba trách nhiệm cần tách rõ
Cookbook validation của Flutter dùng Form để nhóm các field và GlobalKey để truy cập FormState. Validator trả chuỗi lỗi khi chưa hợp lệ, trả null khi hợp lệ. GlobalKey nên được tạo một lần ở State thay vì tạo lại trong mỗi lần build.
validate() chạy các validator và cho biết form hợp lệ hay không. FormState.save gọi lưu trên các field con; callback onSaved nhận giá trị của field. Trong luồng dưới đây, chúng ta chỉ gọi save sau khi validate thành công.
2. Ví dụ một form tự đủ
Đặt vào lib/main.dart của một dự án Flutter thử. Chỉ có một field bắt buộc để bạn tập trung vào đường đi của dữ liệu, không phải vào nhiều quy tắc nghiệp vụ cùng lúc.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ProjectFormPage()));
class ProjectFormPage extends StatefulWidget {
const ProjectFormPage({super.key});
@override
State<ProjectFormPage> createState() => _ProjectFormPageState();
}
class _ProjectFormPageState extends State<ProjectFormPage> {
final _formKey = GlobalKey<FormState>();
String _projectName = '';
String? _validateName(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Bạn cần nhập tên dự án';
}
return null;
}
void _checkInput() {
final form = _formKey.currentState;
if (form == null || !form.validate()) return;
form.save();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Đã kiểm tra: $_projectName')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Kiểm tra tên dự án')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Tên dự án'),
validator: _validateName,
autovalidateMode: AutovalidateMode.onUserInteraction,
onSaved: (value) => _projectName = (value ?? '').trim(),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _checkInput,
child: const Text('Kiểm tra dữ liệu'),
),
],
),
),
),
);
}
}
Thông báo thành công dùng từ “đã kiểm tra” vì ví dụ không lưu dữ liệu bền vững. Đặt thông báo đúng với việc thực sự xảy ra giúp bạn không nhầm một prototype local với một luồng tạo dự án hoàn chỉnh.
3. Chuẩn hóa giá trị một cách nhất quán
Validator dùng trim để coi chuỗi chỉ có khoảng trắng là chưa có tên. OnSaved cũng trim trước khi ghi vào biến, nên điều kiện kiểm tra và giá trị nhận được thống nhất ở điểm này.
Đây là quyết định dành cho tên dự án trong ví dụ. Không sao chép trim cho mọi loại dữ liệu; một số trường có thể cần giữ nguyên khoảng trắng. Nếu có giới hạn độ dài, hãy định nghĩa cách đếm ký tự và kiểm tra Unicode phù hợp thay vì thêm một con số tùy ý vào validator.
Validator nên mô tả điều kiện của field và trả kết quả. Đừng gọi request mạng, thay route hoặc ghi dữ liệu trong validator chỉ vì nó được chạy khi bấm nút. Những hành động đó cần một luồng xử lý riêng có trạng thái chờ và lỗi rõ ràng.
4. Chọn thời điểm hiện lỗi
Trong ví dụ, field dùng AutovalidateMode.onUserInteraction. Người dùng có thể thấy phản hồi sau khi tương tác; nút kiểm tra vẫn gọi validate để xử lý trường hợp chưa nhập gì rồi bấm ngay. Nếu sản phẩm muốn chỉ hiện lỗi sau lần submit đầu, hãy thiết kế chính sách đó riêng và thử lại toàn bộ luồng.
Với nhiều field, ghi thông báo chỉ ra việc cần sửa. “Dữ liệu sai” thường không đủ giúp người dùng biết phải làm gì. Khi có lỗi từ server, phân biệt lỗi field với lỗi chung và tránh xóa nội dung người dùng đã nhập chỉ vì một request thất bại.
Kiểm tra kết quả
Trên emulator hoặc thiết bị, thử chuỗi rỗng, chỉ có dấu cách, tên tiếng Việt bình thường và tên có khoảng trắng hai đầu. Bấm nút trước khi nhập rồi sửa field. Xác nhận lỗi biến mất theo chính sách đã chọn và thông báo sau kiểm tra dùng giá trị đã trim.
Tiếp tục thử khi bàn phím mở và cỡ chữ hệ thống tăng. ScrollView giúp nội dung có đường cuộn, nhưng không tự chứng minh mọi thiết bị đều bố trí tốt; vẫn phải kiểm tra nút và thông báo lỗi không bị che hoặc cắt.
Lỗi thường gặp
Nếu currentState là null, kiểm tra key có gắn đúng Form và callback có chạy khi Form còn trong cây không. Nếu validator không được gọi, kiểm tra bạn dùng TextFormField trong Form thay vì chỉ có TextField. Nếu onSaved chưa chạy, xem nhánh validate có trả false hoặc code đã return trước đó không.
Nếu rebuild làm form giống như mới tạo, tìm GlobalKey được khởi tạo trong build. Khi dữ liệu đã hợp lệ nhưng cần lưu thật, hãy bổ sung luồng lưu có xử lý lỗi theo yêu cầu sản phẩm; không đổi tên callback save thành bằng chứng rằng database đã được cập nhật.
Kết luận
Giữ key ổn định, để validator chỉ ra điều kiện chưa đạt và gọi save có chủ đích sau validate. Một form nhỏ với kỳ vọng rõ ràng giúp bạn phát triển luồng nhập liệu tiếp theo mà không trộn kiểm tra giao diện với việc lưu dữ liệu.


