Custom Shaking Error widget to fully control form field errors in Flutter

Natesh Bhat
Flutter Community
Published in
5 min readOct 3, 2020

More often than not, forms and form fields are coupled with form field validations that validate the text inside the field and show an error if the validation failed.

Before we move into looking at creating a custom animated error widget, let’s see the Good, the Meh and the Bad of default form field provided by flutter.

The Good :

  • Text Form field comes with a validator callback function that allows us to validate the given text on form submission.
  • There is an autovalidate parameter that will keep validating text on every change. (No need for the user to wait until form submission)
TextFormField(
validator : (value){
if(value.isEmpty) return 'Please enter some text';
return null;
},
autoValidate : true
)

The Meh :

  • Since autovalidate validates the text on every keypress, it’s not a good experience for the user.
  • We might end up putting a lot of our business logic inside the validator of each field which harms testability.

The Bad :

  • There is no option to independently control the error shown below the form field based on the logic.
  • No options to control when the error animation (shake/jiggle) happens.
  • Many times we may need to show error text for the whole form or for a completely different widget that doesn’t have a form element but is purely based on business logic or API response.

In this article, we’ll see how to create a fully controllable error widget that you can use anywhere on your screen with or without a form, control its visibility, mounting, animation and text all externally from your business logic components.

Here’s a sample of how its gonna look once we build it.
[ Full code snippet at the end :D ]

Shaking Error Text in action

Creating the ErrorController :

Create a Controller or (ChangeNotifier) that allows us to control the visibility, error text and animation state independently.

We also make revealing with animation and initial visibility optional when the widget is first built. For animation, we use simple_animations to avoid having to manage animation controllers manually.

class ShakingErrorController extends ChangeNotifier {
String _errorText;
bool _isVisible = true;
bool _isMounted = true;
CustomAnimationControl _controlSignal = CustomAnimationControl.PLAY;ShakingErrorController(
{String initialErrorText = 'Error', bool revealWithAnimation = true, bool hiddenInitially = true})
: _errorText = initialErrorText ?? '',
_isVisible = !hiddenInitially,
_controlSignal = (revealWithAnimation ?? true) ? CustomAnimationControl.PLAY : CustomAnimationControl.STOP;
}

Expose methods to change the controller properties :

We add the methods to hide, show, reveal, mount and unmount the error.

class ShakingErrorController extends ChangeNotifier {
...

bool get isMounted => _isMounted;

bool get isVisible => _isVisible;
String get errorText => _errorText;CustomAnimationControl get controlSignal => _controlSignal;set errorText(String errorText) {
_errorText = errorText;
notifyListeners();
}
void onAnimationStarted() {
_controlSignal = CustomAnimationControl.PLAY;
}
void shakeErrorText() {
_controlSignal = CustomAnimationControl.PLAY_FROM_START;
notifyListeners();
}
///fully [unmount] and remove the error text
void unMountError() {
_isMounted = false;
notifyListeners();
}
///[remount] error text. will not be effective if its already mounted
void mountError() {
_isMounted = true;
notifyListeners();
}
///hide the error. but it will still be taking its space.
void hideError() {
_isVisible = false;
notifyListeners();
}
///just shows error without any animation
void showError() {
_isVisible = true;
notifyListeners();
}
///shows error with the reveal [animation]
void revealError() {
showError();
shakeErrorText();
}
}

Making ShakingErrorText widget :

ShakingErrorText widget contains the text and the animation builder to move the error text back and forth to perform the shaking animation.

  • Create the shaking error text widget that takes the controller and the number of times to shake the text as parameters.
  • Using the MultiTween class from simple_animations, we add tweens , each tween corresponding to movement of text along one direction.
enum ErrorAnimationProp { offset }class ShakingErrorText extends StatelessWidget {
final ShakingErrorController controller;
final int timesToShake;
final MultiTween<ErrorAnimationProp> _tween;
ShakingErrorText({
this.controller,
this.timesToShake = 4,
}) : _tween = MultiTween<ErrorAnimationProp>() {
List.generate(
timesToShake,
(_) => _tween
..add(ErrorAnimationProp.offset, Tween<double>(begin: 0, end: 10), Duration(milliseconds: 100))
..add(ErrorAnimationProp.offset, Tween<double>(begin: 10, end: -10), Duration(milliseconds: 100))
..add(ErrorAnimationProp.offset, Tween<double>(begin: -10, end: 0), Duration(milliseconds: 100)));
}
...}
  • Here, we want to move the text from original position to 10 pixels right and then back again to original position and repeat it with left side movement.
  • We repeat adding the above tweens for timesToShake number of times to get the continuous shaking effect.

Building the full widget :
With the tweens in place, we can now build the final widget that consumes the ShakingErrorController and uses the above created tween for translation animation of text.

  • For controlling the animation, we use the CustomAnimation widget from simple_animations.
  • In the animation builder, we translate the error text on x-axis and keep y-axis offset at 0.
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ShakingErrorController>.value(
value: controller,
child: Consumer<ShakingErrorController>(
builder: (context, errorController, child) {
return CustomAnimation<MultiTweenValues<ErrorAnimationProp>>(
control: errorController.controlSignal,
curve: Curves.easeOut,
duration: _tween.duration,
tween: _tween,
animationStatusListener: (status) {
if (status == AnimationStatus.forward) {
controller._onAnimationStarted();
}
},
builder: (BuildContext context, Widget child, tweenValues) {
return Transform.translate(
offset: Offset(tweenValues.get(ErrorAnimationProp.offset), 0),
child: child,
);
},
child: Visibility(
visible: controller.isVisible && controller.isMounted,
maintainSize: controller.isMounted,
maintainAnimation: controller.isMounted,
maintainState: controller.isMounted,
child: Text(
errorController.errorText,
textAlign: TextAlign.start,
style: TextStyle(color: Colors.red)
),
),
);
},
),
);
}

Testing time !!!

Its Show Time

Now that we are done creating our widget, we can test it out with some buttons using the below widget.

Shaking Error Text in action
class TestShakingErrorText extends StatefulWidget {
@override
_TestShakingErrorTextState createState() => _TestShakingErrorTextState();
}

class _TestShakingErrorTextState extends State<TestShakingErrorText> {
final ShakingErrorController controller =
ShakingErrorController(initialErrorText: 'Hello its me. Hi ....', hiddenInitially: false);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
ShakingErrorText(
controller: controller,
),
RaisedButton(
onPressed: () async {
controller.isMounted ? controller.unMountError() : controller.mountError();
},
child: Text('toggle mounting'),
),
RaisedButton(
onPressed: () async {
controller.isVisible ? controller.hideError() : controller.showError();
},
child: Text('toggle visibility'),
),
RaisedButton(
onPressed: () async {
controller.shakeErrorText();
},
child: Text('shake test'),
)
],
),
),
);
}

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

Here’s the full code snippet if you’re lazy ( like me 😉 ) :

gist for shaking error text widget

Don’t forget to Smash that CLAP button 👏🏻 before you go :)

Clapping is Caring :)
https://www.twitter.com/FlutterComm

--

--

Natesh Bhat
Flutter Community

An engineer by passion , developer by profession, singer by heart. You can follow me on https://in.linkedin.com/in/nateshmbhat