flutterdarttextfield

Validation On Stepper Widget


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,
        ],
      ),
    );
  }
}

Solution

  • 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;
        }
    }