neat_form 📋

Một thư viện quản lý trạng thái form và validation gọn nhẹ, mạnh mẽ, type-safe (100% Object?) dành cho Flutter & Dart.

pub package License: MIT Tests: 215 Passed Live Web Demo Zero Dependencies

Tiếng Việt | English (README_EN.md)


📑 Mục Lục


1. Introduction & Overview

neat_form được thiết kế theo tư duy Headless (Zero UI Coupling), State-driven, và Immutable. Package giải phóng bạn khỏi sự phức tạp của form validation trong Flutter, hoạt động độc lập hoặc tích hợp liền mạch với mọi thư viện State Management (Riverpod, BLoC, Cubit, Signals, v.v.).

🌐 Nền tảng hỗ trợ & Yêu cầu

  • Nền tảng: Android, iOS, Web, macOS, Windows, Linux (100% Flutter platforms).
  • SDK: Flutter >= 3.0.0, Dart >= 3.0.0 < 4.0.0.
  • Zero Dependencies: Không dùng thư viện bên ngoài (chỉ dùng Flutter SDK & meta), loại trừ hoàn toàn nguy cơ xung đột phiên bản.

2. Key Features

  • 🚀 Zero UI Coupling (Headless Form): Tách biệt logic và giao diện, tự do tùy biến 100% UI theo Design System riêng.
  • 🧩 UI Builders Suite (NeatFormScope, NeatFieldBuilder): Cung cấp các Widget tiện ích giúp re-render chính xác từng ô input và giảm 70% boilerplate code.
  • 🔒 Type-Safe Tuyệt đối (100% Object? - No dynamic): Bắt lỗi kiểu tĩnh lúc compile-time thông qua Enum keys K.
  • ✈️ Dynamic Form Array: Hỗ trợ đầy đủ biểu mẫu danh sách động (thêm/bớt/sắp xếp hành khách, địa chỉ, mặt hàng) với NeatFormArrayController.
  • ⏱️ Chống Race Condition trong Async Validation: Quản lý token tự động hủy kết quả cũ nếu dữ liệu thay đổi trước khi request mạng hoàn tất.
  • 🌐 Localization Độc lập: Lỗi trả về codeparams, tự động thay thế biến template {minLength}, {maxValue}.
  • 🛠️ 30+ Built-in Validators & Formatters: Đầy đủ từ email, số học, độ tuổi, ngày tháng, hạn thẻ tín dụng, mã màu, JSON cho tới mảng động.

3. Architecture & Data Flow

neat_form phân tách rành mạch 3 tầng: UI LayerState Management LayerCore Logic Engine.

Sơ đồ Kiến trúc

👁️ Xem mã nguồn Mermaid Diagram
flowchart TD
    subgraph UI["🎨 UI Layer (Zero UI Coupling)"]
        Input["TextField / Custom Inputs"]
        Btn["Submit Button / Action UI"]
    end

    subgraph StateMgmt["⚡ State Management Layer"]
        direction TB
        Riverpod["Riverpod Notifier<br/>(NeatFormNotifierMixin)"]
        Bloc["BLoC / Cubit<br/>(NeatFormCubitMixin)"]
        Native["Flutter Native<br/>(NeatFormController)"]
    end

    subgraph CoreEngine["🧠 neat_form Core Engine"]
        direction TB
        Validators["Validation Engine<br/>• 30+ Built-in Rules<br/>• Async Token Engine"]
        FormState["NeatFormState&lt;K&gt;<br/>• Immutable Map&lt;K, NeatFieldState&gt;"]
        Lifecycle["Submission Lifecycle<br/>(idle ➔ submitting ➔ success / failure)"]
        Resolver["NeatErrorResolver<br/>(i18n & Param Interpolation)"]
    end

    Input -->|"1. User types (onChanged)"| StateMgmt
    Btn -->|"2. Trigger submitForm()"| StateMgmt
    StateMgmt -->|"3. Execute validation"| Validators
    Validators -->|"4. Update state & notify"| StateMgmt
    StateMgmt -->|"5. Render updated state"| UI

4. Installation

Thêm neat_form vào file pubspec.yaml:

dependencies:
  neat_form: ^1.3.3

Hoặc chạy lệnh:

flutter pub add neat_form

5. Quick Start Guide

Option 1: NeatForm UI Builders Suite

Bộ Widget UI Builders cung cấp khả năng re-render scoped (chỉ rebuild đúng ô input bị thay đổi) và tự động chia sẻ controller qua BuildContext:

enum LoginFormKey { email, password }

class ModernLoginForm extends StatelessWidget {
  final _form = NeatFormController<LoginFormKey>.fromValues(
    initialValues: {LoginFormKey.email: '', LoginFormKey.password: ''},
    validators: {
      LoginFormKey.email: NeatValidators.email(),
      LoginFormKey.password: NeatValidators.minLength(6),
    },
  );

  @override
  Widget build(BuildContext context) {
    return NeatFormScope<LoginFormKey>(
      controller: _form,
      child: Column(
        children: [
          // 1. Tự lấy controller từ scope và CHỈ rebuild khi email thay đổi!
          NeatFieldBuilder<LoginFormKey, String>(
            field: LoginFormKey.email,
            builder: (context, fieldState, controller) => TextField(
              onChanged: (val) => controller.setField(LoginFormKey.email, val),
              decoration: InputDecoration(
                labelText: 'Email',
                errorText: fieldState.errorMessage,
              ),
            ),
          ),

          // 2. Mật khẩu
          NeatFieldBuilder<LoginFormKey, String>(
            field: LoginFormKey.password,
            builder: (context, fieldState, controller) => TextField(
              obscureText: true,
              onChanged: (val) => controller.setField(LoginFormKey.password, val),
              decoration: InputDecoration(
                labelText: 'Mật khẩu',
                errorText: fieldState.errorMessage,
              ),
            ),
          ),

          // 3. Nút submit tự động quản lý loading spinner & disable khi invalid
          NeatSubmitButton<LoginFormKey>(
            onPressed: (controller) async {
              await controller.submitForm(
                onSubmit: (values) async => print('Login thành công: $values'),
              );
            },
            child: const Text('Đăng nhập'),
          ),
        ],
      ),
    );
  }
}

Option 2: Flutter Native with ListenableBuilder

Nếu bạn muốn tự kiểm soát toàn bộ vòng đời widget:

class NativeLoginFormState extends State<NativeLoginForm> {
  late final NeatFormController<LoginFormKey> _form;

  @override
  void initState() {
    super.initState();
    _form = NeatFormController<LoginFormKey>.fromValues(
      initialValues: {LoginFormKey.email: '', LoginFormKey.password: ''},
      validators: {
        LoginFormKey.email: NeatValidators.email(),
        LoginFormKey.password: NeatValidators.minLength(6),
      },
    );
  }

  @override
  void dispose() {
    _form.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _form,
      builder: (context, _) {
        final email = _form.getField<String>(LoginFormKey.email);
        final password = _form.getField<String>(LoginFormKey.password);

        return Column(
          children: [
            TextField(
              onChanged: (val) => _form.setField(LoginFormKey.email, val),
              decoration: InputDecoration(labelText: 'Email', errorText: email.errorMessage),
            ),
            TextField(
              obscureText: true,
              onChanged: (val) => _form.setField(LoginFormKey.password, val),
              decoration: InputDecoration(labelText: 'Mật khẩu', errorText: password.errorMessage),
            ),
            ElevatedButton(
              onPressed: _form.submissionStatus.isSubmitting
                  ? null
                  : () => _form.submitForm(onSubmit: (v) async => print('Values: $v')),
              child: _form.submissionStatus.isSubmitting
                  ? const CircularProgressIndicator()
                  : const Text('Đăng nhập'),
            ),
          ],
        );
      },
    );
  }
}

Option 3: Riverpod Integration

Sử dụng NeatFormNotifierMixin trong Notifier của Riverpod:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:neat_form/neat_form.dart';

final loginProvider = NotifierProvider<LoginNotifier, NeatFormState<LoginFormKey>>(LoginNotifier.new);

class LoginNotifier extends Notifier<NeatFormState<LoginFormKey>> with NeatFormNotifierMixin<LoginFormKey> {
  @override
  NeatFormState<LoginFormKey> build() {
    return NeatFormState.fromValues({
      LoginFormKey.email: '',
      LoginFormKey.password: '',
    });
  }

  @override
  Map<LoginFormKey, NeatValidator<Object?>> get validators => {
        LoginFormKey.email: NeatValidators.email(),
        LoginFormKey.password: NeatValidators.minLength(6),
      };

  Future<void> submit() async {
    await submitForm(onSubmit: (values) async {
      print('Đăng nhập thành công: $values');
    });
  }
}

Option 4: BLoC & Cubit Integration

Sử dụng NeatFormCubitMixin:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:neat_form/neat_form.dart';

class LoginCubit extends Cubit<NeatFormState<LoginFormKey>> with NeatFormCubitMixin<LoginFormKey> {
  LoginCubit()
      : super(NeatFormState.fromValues({
          LoginFormKey.email: '',
          LoginFormKey.password: '',
        }));

  @override
  Map<LoginFormKey, NeatValidator<Object?>> get validators => {
        LoginFormKey.email: NeatValidators.email(),
        LoginFormKey.password: NeatValidators.minLength(6),
      };

  Future<void> login() async {
    await submitForm(onSubmit: (values) async {
      print('Đăng nhập BLoC: $values');
    });
  }
}

6. Dynamic Form Array (NeatFormArray)

Hỗ trợ các biểu mẫu dạng danh sách (thêm/xóa/sắp xếp nhiều hành khách, địa chỉ, sản phẩm) với ID độc lập và validation từng phần tử:

enum GuestField { fullName, dateOfBirth, passportNo }

final guestsController = NeatFormArrayController<GuestField>(
  initialItems: [
    {GuestField.fullName: 'Nguyễn Văn A', GuestField.dateOfBirth: '15/08/1995', GuestField.passportNo: 'B1234567'},
  ],
  itemValidators: {
    GuestField.fullName: NeatValidators.required(),
    GuestField.dateOfBirth: NeatValidators.dateString(format: 'DD/MM/YYYY', minAge: 18),
    GuestField.passportNo: NeatValidators.required(),
  },
  arrayValidators: [
    NeatArrayValidators.minItems(1, message: 'Cần ít nhất 1 hành khách'),
    NeatArrayValidators.uniqueBy(GuestField.passportNo, message: 'Số hộ chiếu không được trùng nhau'),
  ],
);

// Thao tác CRUD cực kỳ tiện lợi:
guestsController.addItem();             // Thêm một item mới
guestsController.removeItemAt(0);       // Xóa item tại vị trí index
guestsController.reorderItem(0, 2);     // Sắp xếp lại (dùng với ReorderableListView)

7. Form Submission Lifecycle

Vòng đời nộp form

NeatSubmissionStatus gồm 4 trạng thái:

  • idle: Trạng thái ban đầu hoặc sau khi reset.
  • submitting: Đang gọi API xử lý (hiển thị loading spinner).
  • success: Xử lý thành công.
  • failure: Có lỗi xảy ra trong quá trình nộp.

8. Built-in Validators (Cheat Sheet)

Nhóm Tên Validator Mô tả
Cơ bản required() Bắt buộc nhập (chuỗi, số, mảng, map)
custom(predicate) Validator tùy biến theo biểu thức logic
Chuỗi ký tự minLength(min), maxLength(max) Ràng buộc độ dài chuỗi tối thiểu/tối đa
exactLength(len) Ràng buộc chính xác số lượng ký tự
email(), url(), phone() Định dạng Email, URL hợp lệ, Số điện thoại
alpha(), numeric(), alphanumeric() Chỉ chứa chữ cái / chữ số
noWhitespace(), noHtml() Chặn khoảng trắng, chặn mã HTML độc hại
Số học minValue(min), maxValue(max) Giá trị số tối thiểu/tối đa (hỗ trợ cả num & string)
valueRange(min, max) / between Nằm trong khoảng [min, max]
positive(), negative() Số dương ($> 0$), số âm ($< 0$)
nonNegative(), nonPositive() Không âm ($\ge 0$), không dương ($\le 0$)
integerOnly() Chỉ chấp nhận số nguyên
Ngày giờ & Thẻ dateString(format, minAge, maxAge) Ngày tháng theo lịch vạn niên kèm độ tuổi
timeString(format) Chuỗi giờ 24h (HH:mm hoặc HH:mm:ss)
creditCardExpiry() Hạn thẻ tín dụng MM/YY, chặn thẻ hết hạn
Mạng & Định dạng ipv4(), ipv6(), ipAddress() Địa chỉ IP chuẩn
uuid() Chuỗi UUID/GUID v4
hexColor() Mã màu Hex #RGB, #RRGGBB
jsonString() Cú pháp chuỗi JSON hợp lệ
Tập hợp oneOf(allowed), noneOf(forbidden) Giá trị thuộc / không thuộc danh sách
Quan hệ chéo match(targetKey) Trùng khớp với trường khác (ví dụ: Nhập lại mật khẩu)
when(condition, validator) Validate có điều kiện phụ thuộc
Mảng động minItems(min), maxItems(max) Số lượng phần tử tối thiểu/tối đa trong mảng
uniqueBy(fieldKey) Không cho phép trùng lặp giá trị giữa các phần tử

9. Localization & Error Resolver

Tự động ánh xạ mã lỗi thành thông điệp đa ngôn ngữ và nội suy tham số:

final errorResolver = NeatErrorResolver(
  customHandlers: {
    NeatValidators.codeMinLength: (error) => 'Tối thiểu ${error.params['minLength']} ký tự',
  },
  fallbackResolver: (error) => 'Vui lòng kiểm tra lại thông tin',
);

print(errorResolver.resolve(fieldState.error!));

10. Input Formatters & Masking

Định dạng văn bản thời gian thực ngay khi người dùng gõ phím:

// 1. Tiền tệ (VND / USD)
TextField(inputFormatters: [NeatInputFormatters.currency(symbol: '₫', decimalDigits: 0)])

// 2. Thẻ ngân hàng (tự động phân nhóm 4 số)
TextField(inputFormatters: [NeatInputFormatters.creditCard()])

// 3. Mặt nạ chuỗi (Mask)
TextField(inputFormatters: [NeatInputFormatters.mask('####-####-####')])

// 4. Viết hoa & loại bỏ khoảng trắng
TextField(inputFormatters: [NeatInputFormatters.uppercase(), NeatInputFormatters.noSpaces()])

11. Event Tracking & Analytics (NeatFormObserver)

Theo dõi toàn bộ tương tác form để ghi log hoặc gửi dữ liệu phân tích (Telemetry/Analytics):

class AppFormObserver<K> extends NeatFormObserver<K> {
  @override
  void onFieldChanged(K key, Object? value) => print('Field $key đổi sang $value');

  @override
  void onValidationError(K key, NeatValidationError error) => print('Lỗi tại $key: ${error.code}');
}

12. Race-Condition-Free Async Validation

Tự động hủy kết quả kiểm tra cũ khi người dùng tiếp tục gõ phím, loại bỏ hoàn toàn lỗi hiển thị sai trạng thái khi mạng chậm:

await formController.validateFieldAsync(
  LoginFormKey.username,
  (username) async {
    final isTaken = await api.checkUsername(username);
    if (isTaken) return const NeatValidationError('username_taken', message: 'Tên người dùng đã tồn tại');
    return null;
  },
);

13. Flutter DevTools Extension (NeatForm Tab)

neat_form tích hợp sẵn Flutter DevTools Extension chính thức, tự động kích hoạt một tab riêng biệt mang tên NeatForm bên trong Flutter DevTools khi bạn debug ứng dụng.

┌──────────────────┬──────────────────────────────────────────┬─────────────────────────────┐
│ 📋 Form Explorer │ 🔍 Field Inspector: LoginForm            │ ⚡ Telemetry & Actions       │
├──────────────────┼──────────────────────────────────────────┼─────────────────────────────┤
│ 🔍 [Search...]   │ 🏷️ Form ID: LoginForm_8f2a               │ 🚀 Quick Actions:           │
│                  │ 📊 Status: idle | Valid: ✅ | Touched: 1  │ [⚡ Auto-fill Valid]        │
│ 📁 Standard Forms│ ──────────────────────────────────────── │ [⚠️ Fill Boundary Data]     │
│  ├── 🟢 LoginForm│ [Field Name]  [Value]    [Error] [State] │ [🔍 Validate Form Now]      │
│  └── 🔴 Checkout │  email        dat@gm...  -       ✅ valid│ [🔄 Reset Form]             │
│                  │  password     ••••••     -       ✅ valid│ [📥 Import JSON State]      │
│ 📁 Dynamic Arrays│ ──────────────────────────────────────── │ [💾 Export JSON Snapshot]   │
│  └── 🟡 Guests(3)│ ✏️ Live Value Override Dialog:           │ ─────────────────────────── │
│                  │ [ Nhập giá trị mới...       ] [Cập nhật] │ 🕒 Live Event Timeline:     │
└──────────────────┴──────────────────────────────────────────┴─────────────────────────────┘

✨ Tính Năng Vượt Trội:

  1. 📋 Form Explorer: Tự động phát hiện và liệt kê tất cả instance NeatFormController & NeatFormArrayController đang hoạt động trong app (zero config).
  2. 🔍 Field Inspector & Live Value Mutator: Xem chi tiết từng trường (Key, Value, Initial Value, Error Message, Error Code, Touched, Validating state). Cho phép sửa và inject giá trị mới trực tiếp vào thiết bị đang chạy để test tính phản ứng.
  3. Smart Autofill & ⚠️ Boundary Test Generator:
    • ⚡ Fill Valid: Tự động điền dữ liệu đúng chuẩn (Email, Password, SĐT, Ngày sinh...).
    • ⚠️ Fill Boundary: Tự động bơm các giá trị vi phạm biên/lỗi (Email sai định dạng, Password quá ngắn, Số âm, String rỗng) để kiểm thử UI báo lỗi chỉ với 1 click.
  4. 📥 Import & Restore JSON State: Dán bất kỳ snapshot JSON nào để tái hiện chính xác kịch bản lỗi (Bug Reproduction) từ log của người dùng.
  5. 🕒 Live Event Stream Timeline: Theo dõi dòng sự kiện thời gian thực (neat_form:event) kèm mốc thời gian và chi tiết payload.

14. Showcase App

Xem mã nguồn hoàn chỉnh với đầy đủ các tab showcase trong thư mục example/:

cd example
flutter run -d chrome

15. License

Dự án được phát hành theo giấy phép MIT License. Bạn toàn quyền sử dụng, tùy biến và tích hợp vào các dự án thương mại hoàn toàn miễn phí.

Libraries

neat_form
A clean, lightweight, type-safe form state management and validation library for Dart & Flutter.