Flutter Hooks Tutorial

April 11, 2023

#flutter #hooks

Boilerplate Code

Stateful widgets are great and all, but they come with a lot of boilerplate code when dealing with controllers like TextEditingControllers, AnimationControllers, ScrollControllers, and more.

To showcase this, let’s build a basic form in Flutter using a StatefulWidget. We want our TextFormField to have an initial value, so that means we need to remember to first instantiate a TextEditingController and then dispose of the TextEditingController to avoid memory leaks.

// main.dart

class BasicForm extends StatefulWidget {
  
  _BasicFormState createState() => _BasicFormState();
}

class _BasicFormState extends State<BasicForm> {
  late TextEditingController _textController;

  
  void initState() {
    super.initState();
    _textController = TextEditingController(text: 'some text');
  }

  
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    // ...
  }
}

To check if the form is valid, you have to instantiate a global key for the form and assign it as the form key. Then we’re able to validate the form by accessing the form key’s currentState and calling validate. Let’s toss in a state variable called _content, and display it above our TextFormField, making sure it changes whenever the TextFormField’s onChanged fires.

// main.dart

class BasicForm extends StatefulWidget {
  
  _BasicFormState createState() => _BasicFormState();
}

class _BasicFormState extends State<BasicForm> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  // ...

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Form(
        key: _formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_content, style: const TextStyle(fontSize:20)),
            const SizedBox(height: 16),
            TextFormField(
              controller: _textController,
              onChanged: (val) => setState(() => _content = val),
              validator: (val) => val!.isEmpty ? 'Missing text' : null,
            ),
            TextButton(
              onPressed: () => _formKey.currentState!.validate(),
              child: const Text('Validate'),
            ),
          ],
        ),
      ),
    );
  }
}

This doesn’t look too bad, but remember we only have one TextEditingController. Imagine this form had three, five or even ten fields. We’d have to remember to instantiate and dispose of every single controller individually.

Form with Flutter Hooks

However, thanks to the flutter_hooks package, we can reduce this boilerplate down to one line.

# pubspec.yaml

dependencies:
  flutter_hooks: ^0.18.5+1 # or <latest_version>

Let’s make a new widget called HooksForm that extends a HookWidget.

First, we create our formKey using useMemoized, a hook that caches the instance of our key for future use.

Next, we make our textController using useTextEditingController. It handles the instantiation and disposal of the TextEditingController automatically, which means we get to worry about one less thing while building our awesome apps.

Finally, we’ll add a useState hook to store our content string.

Let’s paste our previous code from the old form and fix the names of our variables accordingly. And now our form is working perfectly with less code.

// main.dart

class HooksForm extends HookWidget {
  const HooksForm({super.key});

  
  Widget build(BuildContext context) {
    final formKey = useMemoized(GlobalKey<FormState>.new);
    final textController = useTextEditingController(text: 'some text');
    final context = useState<String>('some text');
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Form(
        key: formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(content.value, style: const TextStyle(fontSize:20)),
            const SizedBox(height: 16),
            TextFormField(
              controller: textController,
              onChanged: (val) => content.value = val),
              validator: (val) => val!.isEmpty ? 'Missing text' : null,
            ),
            TextButton(
              onPressed: () => formKey.currentState!.validate(),
              child: const Text('Validate'),
            ),
          ],
        ),
      ),
    );
  }
}

useEffect

So now you’re probably thinking this is cool and all, but you want to run code as soon as the widget is inserted into the widget tree like how initState does. Luckily, there’s a hook for that.

// main.dart


void build(BuildContext context) {
  useEffect(() {
    print('useEffect callback');
    return null;
  });

  // ...
}

In the build method, we can add useEffect and add an effect callback. This callback is called synchronously on every build, which means if we don’t add any keys, it will always fire when the widget rebuilds.

// main.dart


void build(BuildContext context) {
  // ...

  final content = useState<String>('some text');

  useEffect(
    () {
      print('useEffect callback');
      return null;
    },
    [content.value], // <---
  );

  // ...
}

If we want it to act like initState, then specify an empty list for the keys.

// main.dart


void build(BuildContext context) {
  // ...

  useEffect(
    () {
      print('useEffect callback');
      return null;
    },
    [], // <---
  );

  // ...
}

And if we want the callback to fire only when specific values change, then we can add them to the list. This can be useful in cases where you’re dealing with subscribing to and canceling a subscription to a stream.

useEffect Stream

Wrap Up

There are a lot of reusable hooks that are included in the flutter_hooks package, and you can find out more info on the package page.

You can even write your own hooks for custom functionality and easy code reuse throughout your projects.

💳 Pro Membership


Already a member?  View your courses

❤️ Wall of Love


Well explained! Great instructor, simple and straight-forward.

Katsaros P.

Marcus’s Flutter courses have always been fantastic.

Max

Perfect audio quality. Good pace.

Marlos L.

The excellent feature of this course comparing to many others is the instructor’s explanation during the coding which helps me understand and know how to apply what I’ve learned into practice.

Quang L.

Focused, no waffle, straight-talking, no back-and-forth try-this try-that. WOW. Quality not Quantity. Concise and precise.

Mark T.

As a senior developer, I've learned so much about improving my development process. Marcus is a great instructor, and his coding style is top notch.

L. B.

The presentation is excellent, everything is detailed properly. I recommend Marcus' courses to everyone.

Krisztián S.

Very well explained course, with clear examples. Keep up the great work. A+++!

Fernando B.

Marcus is clear with his instructions and explains what he is doing as he is doing it.

Niki D.