Skip to main content

Form

In this article we will use the complete MobX triad to do form validation. This will involve the creation of a FormStore that contains the observables, actions to mutate the fields and reactions that do validations when a field changes.

info

See the full code here.

Defining the FormStore

This example borrows from the typical sign-up form with fields for username, email and password.

This can be defined with the following Store class.

import 'package:mobx/mobx.dart';

part 'form_store.g.dart';

class FormStore = _FormStore with _$FormStore;

abstract class _FormStore with Store {

String name = '';


String email = '';


String password = '';
}

Make sure to run the build_runner to generate the *.g.dart file. You can do it by running the following command on your project folder. This will watch your files and regenerate on change.flutter pub run build_runner watch --delete-conflicting-outputs

Actions to mutate the fields

The actions that mutate these fields are straightforward. They are invoked whenever the user edits and modifies the TextFields corresponding to the observable-properties.

Auto-actions for property setters

Do note that for single-property changes, you can assign the value directly as in form.name = 'new value'. MobX internally auto-wraps the property setters within an action. For more elaborate mutations, it is advisable to wrap it in an action for atomic updates and notifications. For this example, the code below is only indicative. The property setters are called directly in the Widgets to take advantage of the auto-action-wrapped setters.

abstract class _FormStore with Store {

// ...


void setUsername(String value) {
name = value;
}


void setEmail(String value) {
email = value;
}


void setPassword(String value) {
password = value;
}
}

Validations as side-effects

Validations on the fields is not an explicit action that is invoked on the FormStore. Instead, it is considered a side-effect of a change in one of the fields. In MobX, we can model such side-effects with reactions.

Reactions come in a few flavors but we are going to focus on the aptly named reaction(selector, effect) that takes in two arguments. The first one is a selector-function that monitors the observables used within it. It is supposed to return a value that is compared with a previously returned value. When it changes, it will invoke the effect-function (the second argument).

For the form-validation use case, we want to monitor when any of the fields change value. At that point, we will invoke the side-effect to validate the field. This can be done with three separate reactions, one for each field. You can also observe that these functions are annotated with @action. This is because they are modifying the observable field error, of type FormErrorState and as a principle we never want to do any stray mutations in MobX. Hence the @action annotation.

Notice the use of nesting a Store like FormErrorState inside the parent FormStore. For all practical purposes, think of MobX stores as regular Dart classes with special reactive properties. You can compose these stores the way you like.

abstract class _FormStore with Store {

// ...

final FormErrorState error = FormErrorState();


void validateUsername(String value) {
if (isNull(value) || value.isEmpty) {
error.username = 'Cannot be blank';
return;
}

error.username = null;
}


void validatePassword(String value) {
error.password = isNull(value) || value.isEmpty ? 'Cannot be blank' : null;
}


void validateEmail(String value) {
error.email = isEmail(value) ? null : 'Not a valid email';
}
}

The FormErrorState is defined to capture the String error-message for each field.

class FormErrorState = _FormErrorState with _$FormErrorState;

abstract class _FormErrorState with Store {

String? username;


String? email;


String? password;


bool get hasErrors => username != null || email != null || password != null;
}

See the full list of validators in the validator package package;

Async validations

The validations above are purely synchronous and run entirely on the client. What if you want to check that the username is not colliding with an existing one? This can only be done by looking at the complete list of usernames on the server-side. We can simulate this with a fake async call and use the result to determine if the username is valid.

The validateUsername() action can be tweaked as follows:

abstract class _FormStore with Store {
// ...


ObservableFuture<bool> _usernameCheck = ObservableFuture.value(true);


// ignore: avoid_void_async
Future validateUsername(String value) async {
if (isNull(value) || value.isEmpty) {
error.username = 'Cannot be blank';
return;
}

try {
// Wrap the future to track the status of the call with an ObservableFuture
_usernameCheck = ObservableFuture(checkValidUsername(value));

error.username = null;

final isValid = await _usernameCheck;
if (!isValid) {
error.username = 'Username cannot be "admin"';
return;
}
} on Object catch (_) {
error.username = null;
}

error.username = null;
}
}

In the above code, we wrap the async call (checkValidUsername) within an ObservableFuture. This allows us to observe the change in status and show visual feedback. Rest of the code should look very familiar. You can see that writing such code in MobX is fairly natural and does not require using any special API or abstractions.

Wait, what about the Widgets?

The Widget hierarchy needed here is fairly simple with a list of Observer widgets for each field, wrapped in a Column. It is really not the most interesting part of the example and hence been left out. Do take a look at the source code to see its simplicity.

Summary

This example was a quick exposition on using the three core abstractions of MobX: observables, actions and reactions. The inter-relationships of these constructs is simple and can be picked up in short order. As you explore MobX, you will see there is no rise in the conceptual learning curve. It plateaus very quickly and lets you focus back on building your awesome apps.

info

See the full code here.