neat_form 1.1.0 copy "neat_form: ^1.1.0" to clipboard
neat_form: ^1.1.0 copied to clipboard

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

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: 78 Passed Zero Dependencies

Tiếng Việt | English


🇻🇳 Giới thiệu #

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ợ (Supported Platforms) #

neat_form hoạt động 100% mượt mà trên tất cả các nền tảng được Flutter hỗ trợ:

Platform Hỗ trợ Ghi chú
Android Hỗ trợ mọi phiên bản Android
iOS Hỗ trợ mọi phiên bản iOS
Web Tương thích CanvasKit & HTML renderer
macOS Desktop App
Windows Desktop App
Linux Desktop App

⚙️ Yêu cầu hệ thống (System Requirements) #

  • Flutter SDK: >= 3.0.0
  • Dart SDK: >= 3.0.0 < 4.0.0
  • Zero Third-party Dependencies: Không phụ thuộc bất kỳ thư viện bên ngoài nào (chỉ dùng Flutter SDK & meta), đảm bảo 100% không bao giờ gặp lỗi xung đột phiên bản (No Version Conflicts).

✨ Tính năng nổi bật #

  • 🚀 Zero UI Coupling (Headless Form): Logic form thuần túy, bạn toàn quyền thiết kế giao diện UI theo Design System riêng mà không bị gò bó.
  • 🎯 Native Flutter Integration: NeatFormController kế thừa ChangeNotifier / Listenable, dùng trực tiếp với ListenableBuilder hoặc AnimatedBuilder mà không cần cài thêm thư viện ngoài.
  • 🔒 Type-Safe Tuyệt đối (100% Object? - No dynamic): Bắt lỗi kiểu tĩnh lúc compile-time, an toàn dữ liệu tuyệt đối.
  • 🌐 Localization Độc lập: Lỗi trả về codeparams. NeatErrorResolver tự động thay thế biến template như {minLength}, {maxValue} vào câu thông báo.
  • ⏱️ 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.
  • 🔄 Submission Lifecycle: Tự động quản lý 4 trạng thái nộp form (idle, submitting, success, failure).
  • 🛠️ 25+ Built-in Validators: Đầy đủ từ chuỗi, số học, regex, thẻ tín dụng Luhn, ngày tháng, consent boolean cho tới mảng/danh sách động.

📦 Cài đặt #

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

dependencies:
  neat_form: ^1.1.0-preview.3

Hoặc chạy lệnh:

flutter pub add neat_form

🚀 Hướng dẫn sử dụng nhanh #

⚡ Cách 1: Tích hợp hoàn hảo với Riverpod (Notifier & Freezed)

A. Standalone Form State (Dạng Form Đơn - Siêu Gọn Chỉ 1 Mixin)

Sử dụng NeatFormNotifierMixin<K>chỉ cần 1 mixin duy nhất, không cần viết bất kỳ hàm boilerplate nào:

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

enum LoginFormKey { email, password }

// 1. Notifier siêu sạch: Đúng 1 mixin, KHÔNG boilerplate!
class LoginNotifier extends Notifier<NeatFormState<LoginFormKey>>
    with NeatFormNotifierMixin<LoginFormKey> {
  @override
  NeatFormState<LoginFormKey> build() => NeatFormState.fromValues({
        LoginFormKey.email: '',
        LoginFormKey.password: '',
      });

  @override
  Map<LoginFormKey, NeatValidator<Object?>> get validators => {
        LoginFormKey.email: NeatValidators.combine([
          NeatValidators.required(message: 'Email không được để trống'),
          NeatValidators.email(message: 'Email không hợp lệ'),
        ]),
        LoginFormKey.password: NeatValidators.combine([
          NeatValidators.required(message: 'Mật khẩu không được để trống'),
          NeatValidators.minLength(8, message: 'Tối thiểu 8 ký tự'),
        ]),
      };
}

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

// 2. UI với Surgical Rebuild (Chỉ rebuild đúng ô input bị thay đổi nhờ select)
class EmailInput extends ConsumerWidget {
  const EmailInput({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final email = ref.watch(
      loginNotifierProvider.select((s) => s.field<String>(LoginFormKey.email)),
    );

    return TextField(
      onChanged: (val) => ref.read(loginNotifierProvider.notifier).setField(LoginFormKey.email, val),
      decoration: InputDecoration(
        labelText: 'Email',
        errorText: email.errorMessage, // ✨ Tự động hiển thị message lỗi nếu có
      ),
    );
  }
}
B. Nested / Freezed Screen State (Khi Form nằm bên trong State màn hình)

Nếu bạn dùng Freezed để quản lý State màn hình (LoginScreenState chứa form và các biến khác), dùng NeatNestedFormNotifierMixin<S, K>:

// 1. Khai báo Freezed State
@freezed
class LoginScreenState with _$LoginScreenState {
  const factory LoginScreenState({
    @Default(false) bool isSubmitting,
    @Default(false) bool rememberMe,
    String? serverError,
    required NeatFormState<LoginFormKey> form,
  }) = _LoginScreenState;
}

// 2. Notifier lồng Freezed State mượt mà
class LoginScreenNotifier extends Notifier<LoginScreenState>
    with NeatNestedFormNotifierMixin<LoginScreenState, LoginFormKey> {
  @override
  LoginScreenState build() => LoginScreenState(
        form: NeatFormState.fromValues({
          LoginFormKey.email: '',
          LoginFormKey.password: '',
        }),
      );

  @override
  NeatFormState<LoginFormKey> getForm(LoginScreenState state) => state.form;

  @override
  LoginScreenState updateForm(LoginScreenState state, NeatFormState<LoginFormKey> form) =>
      state.copyWith(form: form);

  @override
  Map<LoginFormKey, NeatValidator<Object?>> get validators => { ... };
}

⚡ Cách 2: Tích hợp với BLoC / Cubit (Hỗ trợ cả Standalone & Freezed)

A. Standalone Cubit Form (Tự động kết nối emit())

Sử dụng NeatFormCubitMixin<K> — không cần override hàm update hay emit thủ công:

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

enum ProfileKey { name, age }

class ProfileCubit extends Cubit<NeatFormState<ProfileKey>>
    with NeatFormCubitMixin<ProfileKey> {
  ProfileCubit()
      : super(
          NeatFormState.fromValues({
            ProfileKey.name: '',
            ProfileKey.age: null,
          }),
        );

  @override
  Map<ProfileKey, NeatValidator<Object?>> get validators => {
        ProfileKey.name: NeatValidators.required(message: 'Tên không được để trống'),
        ProfileKey.age: NeatValidators.combine([
          NeatValidators.required(),
          NeatValidators.minValue(18, message: 'Phải từ 18 tuổi trở lên'),
        ]),
      };

  void onNameChanged(String val) => setAndValidateField(ProfileKey.name, val);
  void onAgeChanged(int? val) => setAndValidateField(ProfileKey.age, val);
}
B. Cubit với Freezed Screen State

Sử dụng NeatNestedFormCubitMixin<S, K> cho Cubit khi State là một Freezed class:

class ProfileCubit extends Cubit<ProfileScreenState>
    with NeatNestedFormCubitMixin<ProfileScreenState, ProfileKey> {
  ProfileCubit() : super(ProfileScreenState(form: NeatFormState.fromValues({ ... })));

  @override
  NeatFormState<ProfileKey> getForm(ProfileScreenState state) => state.form;

  @override
  ProfileScreenState updateForm(ProfileScreenState state, NeatFormState<ProfileKey> form) =>
      state.copyWith(form: form);

  @override
  Map<ProfileKey, NeatValidator<Object?>> get validators => { ... };
}

⚡ Cách 3: Flutter Native với ListenableBuilder (Không cần State Management)

import 'package:flutter/material.dart';
import 'package:neat_form/neat_form.dart';

enum LoginFormKey { email, password }

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

  @override
  State<LoginFormPage> createState() => _LoginFormPageState();
}

class _LoginFormPageState extends State<LoginFormPage> {
  late final NeatFormController<LoginFormKey> _form;

  @override
  void initState() {
    super.initState();
    _form = NeatFormController<LoginFormKey>(
      initialFields: {
        LoginFormKey.email: const NeatFieldState<String>(value: ''),
        LoginFormKey.password: const NeatFieldState<String>(value: ''),
      },
      validators: {
        LoginFormKey.email: NeatValidators.combine([
          NeatValidators.required(message: 'Email không được để trống'),
          NeatValidators.email(message: 'Email không đúng định dạng'),
        ]),
        LoginFormKey.password: NeatValidators.combine([
          NeatValidators.required(message: 'Mật khẩu không được để trống'),
          NeatValidators.minLength(8, message: 'Tối thiểu {minLength} ký tự'),
          NeatValidators.passwordStrength(message: 'Cần ít nhất 1 chữ hoa, 1 số, 1 ký tự đặc biệt'),
        ]),
      },
    );
  }

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

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

        return Column(
          children: [
            TextField(
              onChanged: (val) => _form.setField(LoginFormKey.email, val),
              decoration: InputDecoration(
                labelText: 'Email',
                errorText: emailField.errorMessage,
              ),
            ),
            TextField(
              obscureText: true,
              onChanged: (val) => _form.setField(LoginFormKey.password, val),
              decoration: InputDecoration(
                labelText: 'Mật khẩu',
                errorText: passwordField.errorMessage,
              ),
            ),
            ElevatedButton(
              onPressed: _form.submissionStatus.isSubmitting
                  ? null
                  : () async {
                      await _form.submitForm(
                        onSubmit: (values) async {
                          print('Dữ liệu form hợp lệ: $values');
                        },
                      );
                    },
              child: _form.submissionStatus.isSubmitting
                  ? const CircularProgressIndicator()
                  : const Text('Đăng nhập'),
            ),
          ],
        );
      },
    );
  }
}

📋 Bảng tra cứu Built-in Validators (Cheat Sheet) #

Nhóm Validator Mô tả
Bắt buộc & Chuỗi required() Bắt buộc nhập, không được null/rỗng
notBlank() Không được chỉ chứa toàn khoảng trắng
exactLength(n) Độ dài chuỗi chính xác tuyệt đối $n$ ký tự
minLength(n) Độ dài tối thiểu $n$ ký tự
maxLength(n) Độ dài tối đa $n$ ký tự
lengthRange(min, max) Độ dài nằm trong khoảng $[min, max]$
startsWith(prefix) Bắt đầu bằng tiền tố
endsWith(suffix) Kết thúc bằng hậu tố
contains(sub) / notContains(sub) Chứa hoặc không chứa chuỗi con
latinOnly() Chỉ chứa chữ cái tiếng Anh không dấu
noEmoji() Chặn icon/emoji
Định dạng & Bảo mật email() Kiểm tra định dạng email chuẩn
phone() Số điện thoại (8-15 số, hỗ trợ +)
passwordStrength() Yêu cầu chữ hoa, thường, số, ký tự đặc biệt
creditCard() Kiểm tra số thẻ tín dụng bằng thuật toán Luhn
url() Địa chỉ website (http, https)
numeric() Chuỗi số nguyên hoặc thập phân
alphanumericOnly() Chỉ gồm chữ cái và chữ số
noSpecialChars() Không chứa ký tự đặc biệt
noSpaces() / noLeadingTrailingSpaces() Không có dấu cách / dấu cách ở 2 đầu
blacklist(words) Chặn từ khóa nằm trong danh sách đen
noHtml() Chặn thẻ HTML / script chống XSS
Số học minValue(n) / maxValue(n) Giá trị số tối thiểu / tối đa
positive() / negative() Số dương ($> 0$) hoặc số âm ($< 0$)
multipleOf(step) Bội số chia hết (bước nhảy giá)
decimalPrecision(maxDec) Số lượng chữ số thập phân tối đa
Thời gian & Ngày pastDate() Ngày phải ở quá khứ (ngày sinh)
futureDate() Ngày phải ở tương lai (hạn thẻ)
dateRange(min, max) Ngày nằm trong khoảng cho phép
Boolean & Consent mustBeTrue() Bắt buộc tick (Điều khoản dịch vụ)
mustBeFalse() Bắt buộc là false
Mảng & Danh sách minItems(n) / maxItems(n) Số lượng phần tử tối thiểu / tối đa
uniqueItems() Danh sách không có phần tử trùng lặp
Logic & Tổ hợp match(targetGetter) Khớp với giá trị trường khác (xác nhận mật khẩu)
when(condition, validator) Validate có điều kiện (requiredIf)
combine([v1, v2, ...]) Ghép nhiều luật validate lại với nhau
custom(predicate) Tự viết hàm validate tùy biến nhanh

🌐 Đa ngôn ngữ (Localization & Error Resolver) #

neat_form tách biệt hoàn toàn thông điệp hiển thị khỏi logic. Bạn có thể định nghĩa template nội suy tham số:

final resolver = NeatErrorResolver<BuildContext>();

// Đăng ký bộ dịch theo mã lỗi
resolver.register(
  NeatValidators.codeMinLength,
  (context, params, fieldName) {
    return '$fieldName tối thiểu ${params["minLength"]} ký tự';
  },
);

// Sử dụng tại UI
final errorText = resolver.resolve(context, fieldState.error!, fieldName: 'Mật khẩu');

📊 Giám sát sự kiện & Analytics (Form Observer) #

neat_form cung cấp NeatFormObserver<K> để theo dõi toàn bộ vòng đời form, sự kiện thay đổi giá trị, lỗi validation, và trạng thái submit — lý tưởng cho analytics, telemetry và debug logging:

class AppFormObserver extends NeatFormObserver<LoginFormKey> {
  @override
  void onFieldChanged(LoginFormKey key, Object? value) {
    debugPrint('Field [${key.name}] changed to: $value');
  }

  @override
  void onValidationError(LoginFormKey key, NeatValidationError error) {
    debugPrint('Validation failed on [${key.name}]: ${error.code}');
  }

  @override
  void onSubmissionStatusChanged(NeatSubmissionStatus status) {
    debugPrint('Form submission status: ${status.name}');
  }

  @override
  void onFormSubmitted(Map<LoginFormKey, Object?> values, {required bool isValid}) {
    debugPrint('Form submitted: isValid=$isValid, values=$values');
  }
}

⚡ Kiểm tra bất đồng bộ chống Race-Condition (Async Validation) #

Sử dụng validateFieldAsync với cơ chế sequence token tự động vô hiệu hóa kết quả của các request cũ nếu người dùng tiếp tục gõ phím:

await form.validateFieldAsync<String>(
  SignupFormKey.username,
  (username) async {
    final isTaken = await api.checkUsernameTaken(username);
    if (isTaken) {
      return const NeatValidationError(
        'username_taken',
        message: 'Tên đăng nhập đã tồn tại',
      );
    }
    return null;
  },
);

⚠️ Giới hạn & Câu hỏi thường gặp (Limitations & FAQ) #

1. Package có cung cấp sẵn các Widget giao diện (ví dụ NeatTextField) không?

Không. neat_form tuân thủ nguyên lý Headless Form. Thư viện quản lý state và validation thuần túy, giúp bạn tự do 100% sử dụng với TextField, TextFormField, custom design system, hay bất kỳ thư viện UI nào (shadcn-flutter, flutter_neumorphic, v.v.) mà không bị gò bó style.

2. Làm thế nào để validate Form nhiều bước (Wizard / Multi-step Form)?

Rất đơn giản! Phương thức validateForm cho phép truyền vào danh sách các key của bước hiện tại:

final isStep1Valid = form.validateForm([StepKey.email, StepKey.phone]);

3. Asynchronous Validation có bị gián đoạn hay làm chậm giao diện không?

neat_form tích hợp cơ chế Race Condition Token. Khi người dùng gõ phím liên tục, các kết quả request cũ sẽ tự động bị hủy bỏ và chỉ kết quả mới nhất được cập nhật. Bạn nên kết hợp thêm Timer debounce (như minh họa trong thư mục example/lib/main.dart).


📱 Ứng dụng mẫu (Flutter Showcase App) #

Ứng dụng mẫu đa nền tảng (iOS, Android, Web, macOS) nằm trong thư mục example/.

Chạy ứng dụng mẫu:

cd example
flutter run -d chrome # hoặc -d ios, -d android, -d macos

📝 Giấy phép (License) #

Phát hành dưới giấy phép MIT License.

0
likes
0
points
739
downloads

Publisher

unverified uploader

Weekly Downloads

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

Repository (GitHub)
View/report issues

Topics

#form #validation #form-validation #state-management #riverpod

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on neat_form