Search code examples
flutterdartflutter-layout

How to disable button until Flutter text form field has valid data


I want to disable a button until the text form field is valid. And then once the data is valid the button should be enabled. I have received help on SO previously with a similar question but can't seem to apply what I learned to this problem. The data is valid when the user adds more than 3 characters and fewer than 20. I created a bool (_isValidated) and added it to the validateUserName method calling setState once the user has entered valid data but this is definitely wrong and generates an error message. The error message is:

setState() or markNeedsBuild() called during build.

I can't figure out what I am doing wrong. Any help would be appreciated. Thank you.

class CreateUserNamePage extends StatefulWidget {
  const CreateUserNamePage({
    Key? key,
  }) : super(key: key);

  @override
  _CreateUserNamePageState createState() => _CreateUserNamePageState();
}

class _CreateUserNamePageState extends State<CreateUserNamePage> {
  final _formKey = GlobalKey<FormState>();
  bool _isValidated = false;
  late String userName;
  final TextEditingController _userNameController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _userNameController.addListener(() {
      setState(() {});
    });
  }

  void _clearUserNameTextField() {
    setState(() {
      _userNameController.clear();
    });
  }

  String? _validateUserName(value) {
    if (value!.isEmpty) {
      return ValidatorString.userNameRequired;
    } else if (value.trim().length < 3) {
      return ValidatorString.userNameTooShort;
    } else if (value.trim().length > 20) {
      return ValidatorString.userNameTooLong;
    } else {
      setState(() {
        _isValidated = true;
      });
      return null;
    }
  }

  void _createNewUserName() {
    final form = _formKey.currentState;
    if (form!.validate()) {
      form.save();
    }
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Welcome $userName'),
      ),
    );
    Timer(const Duration(seconds: 2), () {
      Navigator.pop(context, userName);
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final isPortrait =
        MediaQuery.of(context).orientation == Orientation.portrait;
    final screenHeight = MediaQuery.of(context).size.height;
    return WillPopScope(
      onWillPop: () async => false,
      child: Scaffold(
        appBar: CreateUserNameAppBar(
          preferredAppBarSize:
              isPortrait ? screenHeight / 15.07 : screenHeight / 6.96,
        ),
        body: ListView(
          children: [
            Column(
              children: [
                const CreateUserNamePageHeading(),
                CreateUserNameTextFieldTwo(
                  userNameController: _userNameController,
                  createUserFormKey: _formKey,
                  onSaved: (value) => userName = value as String,
                  suffixIcon: _userNameController.text.isEmpty
                      ? const EmptyContainer()
                      : ClearTextFieldIconButton(
                          onPressed: _clearUserNameTextField,
                        ),
                  validator: _validateUserName,
                ),
                CreateUserNameButton(
                  buttonText: ButtonString.createUserName,
                  onPressed: _isValidated ? _createNewUserName : null,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Solution

  • Simply use form validation, inside TextFormField edit validator function , add onChange function and call setState to get inputtedValue that can also keep disable button unless the form is validated.

    Key points to note:

    1. Use final _formKey = GlobalKey<FormState>();
    2. The String? inputtedValue; and !userInteracts() are the tricks, you can refer to the code;
    3. When ElevatedButton onPressed method is null, the button will be disabled. Just pass the condition !userInteracts() || _formKey.currentState == null || !_formKey.currentState!.validate()

    Code here:

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      static const String _title = 'Flutter Code Sample';
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          title: _title,
          home: MyCustomForm(),
        );
      }
    }
    
    class MyCustomForm extends StatefulWidget {
      const MyCustomForm({Key? key}) : super(key: key);
    
      @override
      MyCustomFormState createState() {
        return MyCustomFormState();
      }
    }
    
    // Create a corresponding State class.
    // This class holds data related to the form.
    class MyCustomFormState extends State<MyCustomForm> {
      // Create a global key that uniquely identifies the Form widget
      // and allows validation of the form.
      //
      // Note: This is a GlobalKey<FormState>,
      // not a GlobalKey<MyCustomFormState>.
      final _formKey = GlobalKey<FormState>();
    
      // recording fieldInput
      String? inputtedValue;
    
      // you can add more fields if needed
      bool userInteracts() => inputtedValue != null;
    
      @override
      Widget build(BuildContext context) {
        // Build a Form widget using the _formKey created above.
        return Scaffold(
          appBar: AppBar(
            title: const Text('Form Disable Button Demo'),
          ),
          body: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextFormField(
                  // The validator receives the text that the user has entered.
                  validator: (value) {
                    if (inputtedValue != null && (value == null || value.isEmpty)) {
                      return 'Please enter some text';
                    }
                    return null;
                  },
                  onChanged: (value) => setState(() => inputtedValue = value),
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 16.0),
                  child: ElevatedButton(
                    // return null will disable the button
                    // Validate returns true if the form is valid, or false otherwise.
                    onPressed: !userInteracts() || _formKey.currentState == null || !_formKey.currentState!.validate() ? null :() {
                      // If the form is valid, display a snackbar. In the real world,
                      // you'd often call a server or save the information in a database.
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Processing Data: ' + inputtedValue!)),
                      );
                    },
                    child: const Text('Submit'),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }