I have build a Stepper in my app which is a client form and it has some many inputs and text fields so make them in steps for more organizing but I have an issue with the validation. I looked in the other anthers about this problem and I tried to do the same but it doesn't work. the continue button still goes on to the next step event if the text fields are empty.
here is the code :
class FillClientProfileBody extends StatefulWidget {
const FillClientProfileBody({super.key, required this.id});
final String id;
@override
State<FillClientProfileBody> createState() => _FillClientProfileBodyState();
}
class _FillClientProfileBodyState extends State<FillClientProfileBody> {
final List<GlobalKey<FormState>> formKeys = [
GlobalKey<FormState>(), // ClientDataStep
GlobalKey<FormState>(), // ProjectDataStep
GlobalKey<FormState>(), // WorkDataStep
];
late final TextEditingController _nameController;
late final TextEditingController _phoneController;
late final TextEditingController _emailController;
late final TextEditingController _companyNameController;
late final TextEditingController _dateController;
late final TextEditingController _projectDescriptionController;
late final TextEditingController _mainAxsesController;
ValueNotifier<String?> imageB64 = ValueNotifier(null);
ValueNotifier<String?> fileB64 = ValueNotifier(null);
ValueNotifier<int> currentStep = ValueNotifier(0);
ValueNotifier<String?> selectedProjectType = ValueNotifier(null);
ValueNotifier<String?> selectedCost = ValueNotifier(null);
ValueNotifier<String?> selectedDuration = ValueNotifier(null);
final ValueNotifier<String?> selectedCooperationType = ValueNotifier(null);
@override
void initState() {
_nameController = TextEditingController();
_phoneController = TextEditingController();
_emailController = TextEditingController();
_companyNameController = TextEditingController();
_dateController = TextEditingController();
_projectDescriptionController = TextEditingController();
_mainAxsesController = TextEditingController();
super.initState();
}
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_companyNameController.dispose();
_dateController.dispose();
_projectDescriptionController.dispose();
_mainAxsesController.dispose();
imageB64.dispose();
fileB64.dispose();
currentStep.dispose();
selectedProjectType.dispose();
selectedCost.dispose();
selectedDuration.dispose();
selectedCooperationType.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
TextStrings.fillForm,
style: AppTextStyle.headerSm(),
),
PaddingConfig.h16,
ValueListenableBuilder(
valueListenable: currentStep,
builder: (context, value, child) {
final isLastStep = currentStep.value == getSteps().length - 1;
return Stepper(
steps: getSteps(),
currentStep: value,
onStepContinue: () {
if (formKeys[currentStep.value].currentState!.validate()) {
if (currentStep.value < getSteps().length - 1) {
currentStep.value++;
} else {
context.read<AuthRoleProfileBloc>().add(
UpdateClientProfileEvent(
param: UpdateClientProfileParam(
token: "token",
roleId: widget.id,
image: imageB64.value!,
fullName: _nameController.text,
companyName: _companyNameController.text,
workEmail: _emailController.text,
phoneNumber: _phoneController.text,
projectType: selectedProjectType.value!,
projectDescription: _mainAxsesController.text,
projectCost: selectedCost.value!,
projectDuration: selectedDuration.value!,
projectRequirements:
_projectDescriptionController.text,
documents: fileB64.value!,
cooperationType: selectedCooperationType.value!,
contractTime: _dateController.text,
visibilit: "0",
),
),
);
}
}
},
onStepCancel: () {
currentStep.value == 0 ? null : currentStep.value--;
},
onStepTapped: (value) {
currentStep.value = value;
},
controlsBuilder: (BuildContext context, ControlsDetails details) {
return Row(
children: <Widget>[
ElevatedButton(
onPressed: details.onStepContinue,
child: Text(
isLastStep ? TextStrings.confirm : TextStrings.next,
style: AppTextStyle.buttonStyle(color: AppColors.white),
),
),
PaddingConfig.w16,
if (currentStep.value != 0)
CancelTextButton(
onPressed: details.onStepCancel,
)
],
);
},
);
},
),
PaddingConfig.h16,
],
);
}
List<Step> getSteps() => [
buildCustomStep(
currentStep: currentStep.value,
stepIndex: 0,
title: TextStrings.clientData,
activeColor: currentStep.value > 0
? AppColors.greenShade2
: AppColors.fontLightColor,
content: ClientDataStep(
formKey: formKeys[0],
nameController: _nameController,
phoneController: _phoneController,
imageB64: imageB64.value,
onImageUpdate: (newImage) => imageB64.value = newImage,
),
),
buildCustomStep(
currentStep: currentStep.value,
stepIndex: 1,
title: TextStrings.projectRequest,
activeColor: currentStep.value > 1
? AppColors.greenShade2
: AppColors.fontLightColor,
content: ProjectRequestStep(
formKey: formKeys[1],
selectedProjectType: selectedProjectType,
selectedCost: selectedCost,
selectedDuration: selectedDuration,
projectDescriptionController: _projectDescriptionController,
mainAxsesController: _mainAxsesController,
fileB64: fileB64,
),
),
buildCustomStep(
currentStep: currentStep.value,
stepIndex: 2,
title: TextStrings.workData,
activeColor: currentStep.value > 2
? AppColors.greenShade2
: AppColors.fontLightColor,
content: WorkDataStep(
formKey: formKeys[2],
emailController: _emailController,
companyNameController: _companyNameController,
selectedCooperationType: selectedCooperationType,
dateController: _dateController,
),
),
];
}
and here are my first custom widget ClientDataStep:
import 'package:flutter/material.dart';
import 'package:two_website/config/constants/padding_config.dart';
import 'package:two_website/config/strings/text_strings.dart';
import 'package:two_website/config/theme/text_style.dart';
import 'package:two_website/core/widgets/images/fetch_image_circle.dart';
import 'package:two_website/core/widgets/textfield/custom_phone_number_textfield.dart';
import 'package:two_website/core/widgets/textfield/custom_text_form_field.dart';
class ClientDataStep extends StatelessWidget {
final GlobalKey<FormState> formKey;
final TextEditingController nameController;
final TextEditingController phoneController;
final String? imageB64;
final Function(String? newImage) onImageUpdate;
const ClientDataStep({
super.key,
required this.formKey,
required this.nameController,
required this.phoneController,
required this.imageB64,
required this.onImageUpdate,
});
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.center,
child: FetchImageCircle(
imageB64: imageB64,
onUpdate: onImageUpdate,
),
),
PaddingConfig.h16,
Text("${TextStrings.fullName}*", style: AppTextStyle.bodySm()),
PaddingConfig.h8,
CustomTextFormField(
controller: nameController,
labelText: "",
validator: (p0) => p0 != null ? null : TextStrings.fieldValidation,
),
PaddingConfig.h16,
Text("${TextStrings.phoneNumber}*", style: AppTextStyle.bodySm()),
PaddingConfig.h8,
CustomPhoneNumberField(
phoneController: phoneController,
validator: (p0) => p0 != null && p0.isValidNumber()
? null
: TextStrings.fieldValidation,
),
PaddingConfig.h16,
],
),
);
}
}
The callbacks you are passing as a validator are of the form:
String? validator(String? input) => input != null ? null: 'Error message';
which means that an empty String
is acceptable.
I would suggest making sure that the input is not empty:
String? validator(String? input) {
if (input == null || input.trim().isEmpty) {
return 'Custom error message';
} else {
return null;
}
}