flutter
/

GetX Form Validation: Real-Time Validation with Reactive State

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Form Validation: Real-Time Validation with Reactive State

Introduction to Form Validation with GetX

Form validation is a common requirement in mobile apps. GetX simplifies this by combining its reactive state management with Flutter's form widgets. You can create real‑time validation, manage error messages reactively, and handle submission with minimal boilerplate. This guide covers how to build a reactive login form with validation using GetX.

Real-World Use Cases

  • Login & Signup Forms – Real-time email and password validation with immediate feedback.
  • Checkout Forms – Validate address, credit card details, and phone numbers before submission.
  • Settings Pages – Ensure input values meet requirements (e.g., username uniqueness, password strength).
  • Dynamic Forms – Show/hide fields based on user input (e.g., 'other' option with text field).
  • Multi-step Forms – Validate each step before allowing the user to proceed.

How Form Validation Works with GetX

The flow is straightforward:

  1. Controller holds reactive variables for field values and error messages.
  2. TextField uses onChanged to call validation methods in the controller.
  3. Validation methods update error messages and field values reactively.
  4. Obx in the UI automatically rebuilds to show/hide error messages and enable/disable the submit button.
  5. Submit method checks isFormValid (a computed getter) and proceeds.

This keeps all logic in the controller, leaving the UI clean and purely declarative.

Setting Up the Controller

Create a controller that extends GetxController. Use reactive variables for form fields and error messages. Add validation logic and a submit method.

DARTRead-only
1
class LoginController extends GetxController {
  // Reactive form fields
  var email = ''.obs;
  var password = ''.obs;

  // Reactive error messages
  var emailError = ''.obs;
  var passwordError = ''.obs;

  void validateEmail(String value) {
    email.value = value;
    if (value.isEmpty) {
      emailError.value = 'Email is required';
    } else if (!value.contains('@')) {
      emailError.value = 'Enter a valid email address';
    } else {
      emailError.value = '';
    }
  }

  void validatePassword(String value) {
    password.value = value;
    if (value.isEmpty) {
      passwordError.value = 'Password is required';
    } else if (value.length < 6) {
      passwordError.value = 'Password must be at least 6 characters';
    } else {
      passwordError.value = '';
    }
  }

  // Computed state – automatically recalculated when dependencies change
  bool get isFormValid => emailError.value.isEmpty && passwordError.value.isEmpty && email.value.isNotEmpty && password.value.isNotEmpty;

  void submit() {
    if (isFormValid) {
      // Perform login
      Get.snackbar('Success', 'Logged in with ${email.value}');
    } else {
      Get.snackbar('Error', 'Please fix validation errors');
    }
  }
}

Building the UI

Use Obx to reactively update the UI when validation errors or field values change. TextFields call the controller's validation methods on each change.

DARTRead-only
1
class LoginPage extends GetView<LoginController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: 'Email',
                errorText: controller.emailError.value.isEmpty ? null : controller.emailError.value,
              ),
              onChanged: controller.validateEmail,
            ),
            SizedBox(height: 16),
            TextField(
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Password',
                errorText: controller.passwordError.value.isEmpty ? null : controller.passwordError.value,
              ),
              onChanged: controller.validatePassword,
            ),
            SizedBox(height: 20),
            Obx(() => ElevatedButton(
              onPressed: controller.isFormValid ? controller.submit : null,
              child: Text('Login'),
            )),
          ],
        ),
      ),
    );
  }
}

Using Form State and GlobalKey

Sometimes you may want to use Flutter's Form widget and GlobalKey<FormState> for more advanced validation (e.g., cross-field validation, custom formatters). You can still combine it with GetX.

DARTRead-only
1
class FormController extends GetxController {
  final formKey = GlobalKey<FormState>();
  var email = ''.obs;
  var password = ''.obs;

  String? validateEmail(String? value) {
    if (value == null || value.isEmpty) return 'Email is required';
    if (!value.contains('@')) return 'Invalid email';
    email.value = value;
    return null;
  }

  String? validatePassword(String? value) {
    if (value == null || value.isEmpty) return 'Password is required';
    if (value.length < 6) return 'Minimum 6 characters';
    password.value = value;
    return null;
  }

  void submit() {
    if (formKey.currentState!.validate()) {
      // Form is valid, proceed
      Get.snackbar('Success', 'Welcome ${email.value}');
    }
  }
}

class FormPage extends GetView<FormController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Form with GlobalKey')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: controller.formKey,
          child: Column(
            children: [
              TextFormField(
                decoration: InputDecoration(labelText: 'Email'),
                validator: controller.validateEmail,
              ),
              TextFormField(
                decoration: InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: controller.validatePassword,
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: controller.submit,
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Reactive Validation with Workers

You can also use workers to react to form field changes and perform validation in a more centralized way.

DARTRead-only
1
class AdvancedController extends GetxController {
  var email = ''.obs;
  var password = ''.obs;
  var emailError = ''.obs;
  var passwordError = ''.obs;

  @override
  void onInit() {
    super.onInit();
    ever(email, validateEmail);
    ever(password, validatePassword);
  }

  void validateEmail(String value) {
    if (value.isEmpty) emailError.value = 'Required';
    else if (!value.contains('@')) emailError.value = 'Invalid email';
    else emailError.value = '';
  }

  void validatePassword(String value) {
    if (value.isEmpty) passwordError.value = 'Required';
    else if (value.length < 6) passwordError.value = 'Minimum 6 chars';
    else passwordError.value = '';
  }
}

Comparison: GetX Reactive Form vs Flutter Form with GlobalKey

AspectGetX Reactive (Obs + Obx)Flutter Form (GlobalKey)
ReactivityReal‑time, automatic UI updatesValidation only on submit or manually triggered
BoilerplateMinimal (no GlobalKey)Slightly more (requires GlobalKey and validator functions)
Error DisplayImmediate as user typesUsually after submit or on field focus lost
Cross‑field validationEasy – use computed gettersPossible but requires manual handling
PerformanceGranular rebuilds with ObxWhole form rebuilds on submit

Choose the GetX reactive approach for real‑time feedback and simpler code. Use Flutter's Form widget when you need built‑in features like autovalidateMode or when you're migrating existing code.

Best Practices

  • Keep validation logic in the controller – Not in the widget. This improves testability and separation of concerns.
  • Use reactive variables for field values and errors – So the UI updates automatically.
  • Consider using debounce for expensive validations – If validating against a server, use debounce to avoid excessive calls.
  • Use GetView to get the controller – Cleaner than Get.find inside the widget.
  • Provide immediate feedback – Show errors as the user types, or after they leave the field, depending on UX.
  • Disable submit button until form is valid – Prevents invalid submissions and improves UX.
  • Use computed getters for derived state – Like isFormValid to avoid duplicating logic.

Common Mistakes

  • ❌ Using TextEditingController in addition to GetX reactive variables – Duplicate state can lead to inconsistencies. ✅ Use only reactive variables for field values; update them via onChanged.
  • ❌ Calling validate methods inside build – Triggers validation on every rebuild. ✅ Use onChanged or workers to validate when needed.
  • ❌ Not resetting error messages when field becomes valid – The error will remain. ✅ Clear error messages when validation passes.
  • ❌ Using Form and GlobalKey without handling onChanged – The reactive variables won't update until validation, which only happens on submit. ✅ Use onChanged to keep reactive values in sync.

Next Steps

  • 👉 Master GetX Reactive State for more complex form handling
  • 👉 Learn GetX Workers to react to field changes
  • 👉 Explore GetX Dependency Injection to inject validation services

Conclusion

GetX simplifies form validation by letting you use reactive variables for fields and errors, and by providing a clean separation between UI and validation logic. Whether you choose simple reactive validation or combine with Flutter's Form widget, GetX helps you build responsive, user‑friendly forms with minimal code.

Try it yourself

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: LoginPage(),
      initialBinding: LoginBinding(),
    );
  }
}

class LoginController extends GetxController {
  var email = ''.obs;
  var password = ''.obs;
  var emailError = ''.obs;
  var passwordError = ''.obs;

  void validateEmail(String value) {
    email.value = value;
    if (value.isEmpty) {
      emailError.value = 'Email is required';
    } else if (!value.contains('@')) {
      emailError.value = 'Enter a valid email address';
    } else {
      emailError.value = '';
    }
  }

  void validatePassword(String value) {
    password.value = value;
    if (value.isEmpty) {
      passwordError.value = 'Password is required';
    } else if (value.length < 6) {
      passwordError.value = 'Password must be at least 6 characters';
    } else {
      passwordError.value = '';
    }
  }

  bool get isFormValid => emailError.value.isEmpty && passwordError.value.isEmpty && email.value.isNotEmpty && password.value.isNotEmpty;

  void submit() {
    if (isFormValid) {
      Get.snackbar('Success', 'Logged in with ${email.value}');
    } else {
      Get.snackbar('Error', 'Please fix validation errors');
    }
  }
}

class LoginBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => LoginController());
  }
}

class LoginPage extends GetView<LoginController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: 'Email',
                errorText: controller.emailError.value.isEmpty ? null : controller.emailError.value,
              ),
              onChanged: controller.validateEmail,
            ),
            SizedBox(height: 16),
            TextField(
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Password',
                errorText: controller.passwordError.value.isEmpty ? null : controller.passwordError.value,
              ),
              onChanged: controller.validatePassword,
            ),
            SizedBox(height: 20),
            Obx(() => ElevatedButton(
              onPressed: controller.isFormValid ? controller.submit : null,
              child: Text('Login'),
            )),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

What is the recommended way to store form field values in a GetX controller?

A
TextEditingController
B
Reactive variables (`.obs`)
C
GlobalKey<FormState>
D
StatefulWidget state
Q2
of 3

How can you automatically validate a field as the user types?

A
Using `onChanged` to call a validation method
B
Using a timer
C
Using `onEditingComplete` only
D
Using `Form`'s autovalidate mode
Q3
of 3

What is the purpose of the `isFormValid` getter in the controller?

A
To enable/disable the submit button
B
To validate the entire form on submit
C
To reset the form
D
To show loading indicator

Frequently Asked Questions

Should I use `TextEditingController` or reactive variables?

Prefer reactive variables (.obs) to keep state in the controller. TextEditingController can be used, but then you have two sources of truth. If you need advanced text editing features, you can sync both, but reactive variables are simpler.

How do I handle form submission and loading state?

Add an isLoading reactive variable, show a progress indicator when it's true, and disable the submit button.

Can I use GetX with Flutter's `Form` widget?

Yes, as shown in the second example. You can keep the GlobalKey in the controller and call validate() inside the submit method.

How do I reset the form after submission?

Set the reactive field values to empty strings and clear error messages.

What about server-side validation errors?

After API call, you can set error messages in the controller (e.g., emailError.value = 'Email already taken').

Previous

getx getview getwidget

Next

getx internationalization

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.