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.
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.
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
likeFormErrorState
inside the parentFormStore
. 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;
}
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.
See the full code here.