π Dokumentasi Form & Validation β Flutter Starter Kit
π¦ Package yang Digunakanβ
Untuk membangun form yang konsisten, kita pakai beberapa package utama:
flutter_form_builder
β Memberikan abstraction lebih baik untuk form, validator built-in,FormBuilderState
, dan integrasi mudah untuk error handling.form_builder_validators
β Kumpulan validator siap pakai (required, email, minLength, dsb).flutter_bloc
β Untuk state management dan event-driven form submission.freezed
β Untuk membuatsealed class
state/event yang clean.injectable
+get_it
β Untuk dependency injection, supaya tiap layer tidak saling ketergantungan secara langsung.
π Validation Input: Client vs Serverβ
β Client-side validationβ
Dilakukan langsung di UI sebelum request dikirim ke server.
Contoh rules umum:
- Name: required, minLength(3)
- Email: required, format email valid
- Password: required, minLength(8), harus ada angka/huruf besar
Tujuan: mengurangi request ke server untuk input yang jelas-jelas salah.
π Server-side validationβ
Dilakukan setelah request dikirim ke API.
Server bisa mengembalikan error per-field atau global error.
Contoh (response JSON Laravel/Express/NestJS):
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email has already been taken."],
"password": ["Password must contain at least one symbol."]
}
}
π Best practice:
- Client tetap validasi, meski server juga validasi (defense in depth).
- UI harus siap menerima per-field errors dari server, lalu ditampilkan langsung di form.
ποΈ Form Validation Mode di Flutterβ
Flutter (dan FormBuilder
) menyediakan tiga mode utama untuk menampilkan error:
AutovalidateMode.disabled
β Error hanya muncul setelahformKey.currentState!.validate()
dipanggil (biasanya saat submit).AutovalidateMode.onUserInteraction
β Error baru muncul setelah user menyentuh field minimal sekali, lalu keluar dari field.AutovalidateMode.always
β Error ditampilkan secara realtime setiap kali user mengetik (cocok untuk UX interaktif, tapi kadang terasa βjudgyβ kalau dipakai untuk semua field).
π Rekomendasi Starter Kit:
- Gunakan
onUserInteraction
untuk form register/login (balance UX). - Gunakan
always
untuk field sensitif (misalnya password strength meter). - Gunakan
disabled
hanya kalau kamu ingin error muncul sekali saja saat submit.
ποΈ Flow Arsitekturβ
UI (RegisterPage, FormBuilder)
β Bloc (RegisterBloc)
β UseCase (Register)
β Repository (AuthRepository)
β RemoteDataSource (AuthRemoteDataSource)
β LocalDataSource (AuthLocalDataSource)
π§ UseCase (Register)β
()
class Register {
final AuthRepository repository;
Register(this.repository);
Future<Either<Failure, UserEntity>> call({
required String name,
required String email,
required String password,
}) {
return repository.register(
name: name,
email: email,
password: password,
);
}
}
ποΈ Blocβ
sealed class RegisterEvent {}
class RegisterSubmitted extends RegisterEvent {
final String name;
final String email;
final String password;
RegisterSubmitted(this.name, this.email, this.password);
}
class RegisterState with _$RegisterState {
const factory RegisterState.initial() = _Initial;
const factory RegisterState.loading() = _Loading;
const factory RegisterState.success(UserEntity user) = _Success;
const factory RegisterState.failure(Failure failure) = _Failure;
}
()
class RegisterBloc extends Bloc<RegisterEvent, RegisterState> {
final Register register;
RegisterBloc(this.register) : super(const RegisterState.initial()) {
on<RegisterSubmitted>(_onSubmitted);
}
Future<void> _onSubmitted(
RegisterSubmitted event,
Emitter<RegisterState> emit,
) async {
emit(const RegisterState.loading());
final result = await register(
name: event.name,
email: event.email,
password: event.password,
);
result.fold(
(failure) => emit(RegisterState.failure(failure)),
(user) => emit(RegisterState.success(user)),
);
}
}
πΌοΈ UI (Register Page)β
class RegisterPage extends StatelessWidget {
RegisterPage({super.key});
final _formKey = GlobalKey<FormBuilderState>();
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<RegisterBloc>(),
child: Scaffold(
appBar: AppBar(title: const Text("Register")),
body: BlocConsumer<RegisterBloc, RegisterState>(
listener: (context, state) {
state.whenOrNull(
success: (user) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Register success!")),
);
},
failure: (failure) {
if (failure is ValidationFailure) {
// Error per-field dari server
final errors = failure.errors;
errors.forEach((field, messages) {
_formKey.currentState?.invalidateField(
name: field,
errorText: messages.join(", "),
);
});
} else {
// Error global
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(failure.toString())),
);
}
},
);
},
builder: (context, state) {
final bloc = context.read<RegisterBloc>();
return Padding(
padding: const EdgeInsets.all(16),
child: FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
FormBuilderTextField(
name: "name",
decoration: const InputDecoration(labelText: "Name"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.minLength(3),
]),
),
FormBuilderTextField(
name: "email",
decoration: const InputDecoration(labelText: "Email"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
),
FormBuilderTextField(
name: "password",
decoration: const InputDecoration(labelText: "Password"),
obscureText: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.minLength(8),
]),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: state is _Loading
? null
: () {
if (_formKey.currentState?.saveAndValidate() ??
false) {
final values = _formKey.currentState!.value;
bloc.add(RegisterSubmitted(
values['name'],
values['email'],
values['password'],
));
}
},
child: state is _Loading
? const CircularProgressIndicator()
: const Text("Register"),
),
],
),
),
);
},
),
),
);
}
}
π Ringkasan Best Practiceβ
- Selalu pakai client-side validation (UX lebih baik).
- Selalu tangani server-side validation (supaya UI tidak crash kalau ada error spesifik server).
- Gunakan
AutovalidateMode.onUserInteraction
sebagai default. - Tangani error per-field server dengan
invalidateField
. - Simpan logika validasi berat di server (misalnya email sudah terdaftar, password strength rules).