Flutter GetX forms validation
Asked Answered
I

2

18

I am looking for an example of how to handle forms and validation in best practice with GetX? Is there any good example of that or can someone show me an example of how we best can do this?

Impecunious answered 26/10, 2020 at 20:31 Comment(1)
you can see getx extension for vs code. It has some snippets for getx, also getx forms. marketplace.visualstudio.com/…Dianadiandra
T
23

Here's an example of how you could use GetX's observables to dynamically update form fields & submit button.

I make no claim that this is a best practice. I'm sure there's better ways of accomplishing the same. But it's fun to play around with how GetX can be used to perform validation.

Form + Obx

Two widgets of interest that rebuild based on Observable value changes:

  1. TextFormField
    • InputDecoration's errorText changes & will rebuild this widget
    • onChanged: fx.usernameChanged doesn't cause rebuilds. This calls a function in the controller usernameChanged(String val) when form field input changes.
    • It just updates the username observable with a new value.
    • Could be written as:
    • onChanged: (val) => fx.username.value = val
  2. ElevatedButton (a "Submit" button)
    • onPressed function can change between null and a function
    • null disables the button (only way to do so in Flutter)
    • a function here will enable the button
class FormObxPage extends StatelessWidget {
  const FormObxPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    FormX fx = Get.put(FormX()); // controller

    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Validation'),
      ),
      body: SafeArea(
        child: Container(
          alignment: Alignment.center,
          margin: const EdgeInsets.symmetric(horizontal: 5),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Obx(
                    () {
                  print('rebuild TextFormField ${fx.errorText.value}');
                  return TextFormField(
                      onChanged: fx.usernameChanged, // controller func
                      decoration: InputDecoration(
                          labelText: 'Username',
                          errorText: fx.errorText.value // obs
                      )
                  );
                },
              ),
              Obx(
                    () => ElevatedButton(
                  child: const Text('Submit'),
                  onPressed: fx.submitFunc.value, // obs
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

GetX Controller

Explanation / breakdown below

class FormX extends GetxController {
  RxString username = RxString('');
  RxnString errorText = RxnString(null);
  Rxn<Function()> submitFunc = Rxn<Function()>(null);

  @override
  void onInit() {
    super.onInit();
    debounce<String>(username, validations, time: const Duration(milliseconds: 500));
  }

  void validations(String val) async {
    errorText.value = null; // reset validation errors to nothing
    submitFunc.value = null; // disable submit while validating
    if (val.isNotEmpty) {
      if (lengthOK(val) && await available(val)) {
        print('All validations passed, enable submit btn...');
        submitFunc.value = submitFunction();
        errorText.value = null;
      }
    }
  }

  bool lengthOK(String val, {int minLen = 5}) {
    if (val.length < minLen) {
      errorText.value = 'min. 5 chars';
      return false;
    }
    return true;
  }

  Future<bool> available(String val) async {
    print('Query availability of: $val');
    await Future.delayed(
        const Duration(seconds: 1),
            () => print('Available query returned')
    );

    if (val == "Sylvester") {
      errorText.value = 'Name Taken';
      return false;
    }
    return true;
  }

  void usernameChanged(String val) {
    username.value = val;
  }

  Future<bool> Function() submitFunction() {
    return () async {
      print('Make database call to create ${username.value} account');
      await Future.delayed(const Duration(seconds: 1), () => print('User account created'));
      return true;
    };
  }
}

Observables

Starting with the three observables...

  RxString username = RxString('');
  RxnString errorText = RxnString(null);
  Rxn<Function()> submitFunc = Rxn<Function()>(null);

username will hold whatever was last input into the TextFormField.

errorText is instantiated with null initial value so the username field is not "invalid" to begin with. If not null (even empty string), TextFormField will be rendered red to signify invalid input. When a non-valid input is in the field, we'll show an error message. (min. 5 chars in example:)

username too short

submitFunc is an observable for holding a submit button function or null, since functions in Dart are actually objects, this is fine. The null value initial assignment will disable the button.

onInit

The debounce worker calls the validations function 500ms after changes to the username observable end.

validations will receive username.value as its argument.

More on workers.

Validations

Inside validations function we put any types of validation we want to run: minimum length, bad characters, name already taken, names we personally dislike due to childhood bullies, etc.

For added realism, the available() function is async. Commonly this would query a database to check username availability so in this example, there's a fake 1 second delay before returning this validation check.

submitFunction() returns a function which will replace the null value in submitFunc observable when we're satisfied the form has valid inputs and we allow the user to proceed.

A little more realistic, we'd prob. expect some return value from the submit button function, so we could have the button function return a future bool:

  Future<bool> Function() submitFunction() {
    return () async {
      print('Make database call to create ${username.value} account');
      await Future.delayed(Duration(seconds: 1), () => print('User account created'));
      return true;
    };
  }
Tao answered 18/12, 2020 at 23:16 Comment(0)
S
24

GetX is not the solution for everything but it has some few utility methods which can help you achieve what you want. For example you can use a validator along with SnackBar for final check. Here is a code snippet that might help you understand the basics.

TextFormField(
  controller: emailController,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: (value) {
    if (!GetUtils.isEmail(value))
      return "Email is not valid";
    else
      return null;
  },               
),

GetUtils has few handy methods for quick validations and you will have to explore each method to see if it fits your need.

Sardanapalus answered 16/12, 2020 at 12:49 Comment(1)
Awesome comment, using GetUtils and autovalidatemode make this simpler!!Syndic
T
23

Here's an example of how you could use GetX's observables to dynamically update form fields & submit button.

I make no claim that this is a best practice. I'm sure there's better ways of accomplishing the same. But it's fun to play around with how GetX can be used to perform validation.

Form + Obx

Two widgets of interest that rebuild based on Observable value changes:

  1. TextFormField
    • InputDecoration's errorText changes & will rebuild this widget
    • onChanged: fx.usernameChanged doesn't cause rebuilds. This calls a function in the controller usernameChanged(String val) when form field input changes.
    • It just updates the username observable with a new value.
    • Could be written as:
    • onChanged: (val) => fx.username.value = val
  2. ElevatedButton (a "Submit" button)
    • onPressed function can change between null and a function
    • null disables the button (only way to do so in Flutter)
    • a function here will enable the button
class FormObxPage extends StatelessWidget {
  const FormObxPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    FormX fx = Get.put(FormX()); // controller

    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Validation'),
      ),
      body: SafeArea(
        child: Container(
          alignment: Alignment.center,
          margin: const EdgeInsets.symmetric(horizontal: 5),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Obx(
                    () {
                  print('rebuild TextFormField ${fx.errorText.value}');
                  return TextFormField(
                      onChanged: fx.usernameChanged, // controller func
                      decoration: InputDecoration(
                          labelText: 'Username',
                          errorText: fx.errorText.value // obs
                      )
                  );
                },
              ),
              Obx(
                    () => ElevatedButton(
                  child: const Text('Submit'),
                  onPressed: fx.submitFunc.value, // obs
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

GetX Controller

Explanation / breakdown below

class FormX extends GetxController {
  RxString username = RxString('');
  RxnString errorText = RxnString(null);
  Rxn<Function()> submitFunc = Rxn<Function()>(null);

  @override
  void onInit() {
    super.onInit();
    debounce<String>(username, validations, time: const Duration(milliseconds: 500));
  }

  void validations(String val) async {
    errorText.value = null; // reset validation errors to nothing
    submitFunc.value = null; // disable submit while validating
    if (val.isNotEmpty) {
      if (lengthOK(val) && await available(val)) {
        print('All validations passed, enable submit btn...');
        submitFunc.value = submitFunction();
        errorText.value = null;
      }
    }
  }

  bool lengthOK(String val, {int minLen = 5}) {
    if (val.length < minLen) {
      errorText.value = 'min. 5 chars';
      return false;
    }
    return true;
  }

  Future<bool> available(String val) async {
    print('Query availability of: $val');
    await Future.delayed(
        const Duration(seconds: 1),
            () => print('Available query returned')
    );

    if (val == "Sylvester") {
      errorText.value = 'Name Taken';
      return false;
    }
    return true;
  }

  void usernameChanged(String val) {
    username.value = val;
  }

  Future<bool> Function() submitFunction() {
    return () async {
      print('Make database call to create ${username.value} account');
      await Future.delayed(const Duration(seconds: 1), () => print('User account created'));
      return true;
    };
  }
}

Observables

Starting with the three observables...

  RxString username = RxString('');
  RxnString errorText = RxnString(null);
  Rxn<Function()> submitFunc = Rxn<Function()>(null);

username will hold whatever was last input into the TextFormField.

errorText is instantiated with null initial value so the username field is not "invalid" to begin with. If not null (even empty string), TextFormField will be rendered red to signify invalid input. When a non-valid input is in the field, we'll show an error message. (min. 5 chars in example:)

username too short

submitFunc is an observable for holding a submit button function or null, since functions in Dart are actually objects, this is fine. The null value initial assignment will disable the button.

onInit

The debounce worker calls the validations function 500ms after changes to the username observable end.

validations will receive username.value as its argument.

More on workers.

Validations

Inside validations function we put any types of validation we want to run: minimum length, bad characters, name already taken, names we personally dislike due to childhood bullies, etc.

For added realism, the available() function is async. Commonly this would query a database to check username availability so in this example, there's a fake 1 second delay before returning this validation check.

submitFunction() returns a function which will replace the null value in submitFunc observable when we're satisfied the form has valid inputs and we allow the user to proceed.

A little more realistic, we'd prob. expect some return value from the submit button function, so we could have the button function return a future bool:

  Future<bool> Function() submitFunction() {
    return () async {
      print('Make database call to create ${username.value} account');
      await Future.delayed(Duration(seconds: 1), () => print('User account created'));
      return true;
    };
  }
Tao answered 18/12, 2020 at 23:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.