When it comes to managing the lifecycle of a widget in Flutter, StatefulWidget is the default approach. While it works well, it can have disadvantages, such as remembering to call the dispose() function and duplicate code when using the widget multiple times. In this article, I’ll introduce Flutter Hooks, a package that provides an alternative solution to StatefulWidget. We’ll explore how to use HookWidget, pre-defined hooks like useState and useEffect, and create custom hooks. By the end of this article, you’ll better understand how to use Flutter Hooks to write cleaner, more efficient code in your Flutter projects.

What do you need to know about  StatefulWidget?

The default way to manage the lifecycle of a widget in Flutter is StatefulWidget, and it’s a pretty good solution. However, this approach has several disadvantages that force the developer to remember about calling dispose() function every time it’s needed and duplicate many lines of code every time they want to use this particular widget. 

Here is an example of StatefulWidget that every Flutter developer knows. It comes with the counter app, the starting point of every Flutter project created

As you can see, using StatefulWidgets requires overriding many methods and adding state definition and implementation. This might get cumbersome when we have multiple widgets of this type because we have to repeat this non-reusable code repeatedly.

The example below shows how to create a screen with two text fields using StatefulWidget.

import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  final firstTextController = TextEditingController();
  final secondTextController = TextEditingController();
  @override
  void initState() {
    super.initState();
    firstTextController.text = 'First text';
    secondTextController.text = 'Second text';
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              TextFormField(
                controller: firstTextController,
                onChanged: (value) => setState(() {}),
              ),
              const SizedBox(height: 24),
              TextFormField(
                controller: secondTextController,
                onChanged: (value) => setState(() {}),
              ),
              const SizedBox(height: 32),
              Text(firstTextController.text),
              const SizedBox(height: 32),
              Text(secondTextController.text),
            ],
          ),
        ),
      ),
    );
  }

We must also remember to close every used controller inside dispose() method to prevent memory leaks.

  @override
  void dispose() {
    firstTextController.dispose();
    secondTextController.dispose();
    super.dispose();
  }
}

As you can see, StatefulWidget requires overriding the initState() and dispose() methods. Also, we have to use two related classes: MyHomePage and private class _MyHomePageState.

How to use Flutter Hooks?

There is another solution available. Flutter Hooks is a package maintained by Remi Rousselet – the same person responsible for Freezed and Riverpod packages.

Let’s consider a situation in which we have the same functionality but use the flutter_hooks package:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class HookPage extends HookWidget {
  final String title;
  const HookPage({
    super.key,
    required this.title,
  });
  @override
  Widget build(BuildContext context) {
    final firstTextController = useTextEditingController(text: 'First text');
    final secondTextController = useTextEditingController(text: 'Second text');
    useListenable(firstTextController);
    useListenable(secondTextController);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Form(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                TextFormField(
                  controller: firstTextController,
                  onChanged: (value) => value = firstTextController.text,
                ),
                const SizedBox(height: 24),
                TextFormField(
                  controller: secondTextController,
                  onChanged: (value) => value = secondTextController.text,
                ),
                const SizedBox(height: 32),
                Text(firstTextController.text),
                const SizedBox(height: 32),
                Text(secondTextController.text),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

The output of both approaches is identical, we receive a screen with two text fields with an initial value of “First text” and “Second text”. If we modify, inputs in the text fields text below will automatically update. 

As you can see, we managed to keep the same functionality, using much less code and, importantly, using only one class – HookPage, which extends HookWidget. 

HookWidget

HookWidget is a new type of widget provided with the package. Structurally, it’s similar to StatelessWidget because all the content is in one class, and there is no state definition or implementation, it only implements the build method, but it’s not associated with an Element. If we check the documentation, it turns out that HookWidget extends StatelessWidget, not StatefulWidget, as one might expect based on its functionality. So how does it work? 

The difference between StatelessWidget and HookWidget is that it can use Hook, an object which allows HookWidget to store mutable data without implementing a State. A hook is equivalent to State for StatefulWidget, with the notable difference that a HookWidget uses a list of hooks so that it can have more than one Hook. The Hook is created within the HookState.build method of the HookWidget, and the creation must be done unconditionally, always in the same order. In practice, this means that hooks cannot be called inside conditions like if/else statements and should always be created inside the build() method. You also cannot finish calling your build without calling every one of your hooks. The easiest way to use your hook right is to call all of them at the beginning of the build method, then use their values and draw a widget tree on the screen. Just like I did in the example above. 

But what if you do not follow these instructions and call your hooks outside of build() method or, for example in StatelessWidget instead of HookWidget? Simply you will get runtime errors 🙂 

Commonly used hooks in Flutter development

Along with the package, we get a few pre-prepared hooks. They are super functional, and in many cases, they are sufficient when creating smaller and medium-sized applications. But as you know, it all depends on the specific situation. That is why there is also the possibility of creating your custom hooks, which is a great way to save time when a given hook is supposed to be used many times.

Probably the most used hook is useState. useState uses ValueNotifier under the hood to create value and subscribe to it. It is equivalent to a default setState, so each state change leads to a rebuild of the widget. This is a perfect solution to manage the local state of a single widget. 

Let us consider the Flutter template project I mentioned at the beginning.

import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
  const CounterPage(this.title, {super.key});
  final String title;
  @override
  State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

This is how it looks like using StatefulWidget. Two classes were used, and the state was defined and implemented. Each time the user presses the button, the _incrementCounter method is called, and with it also setState() to rebuild the widget and show state change on the screen. 

Let’s check how it looks when using useState:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterPage extends HookWidget {
  const CounterPage({
    required this.title,
    super.key,
  });
  final String title;
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              counter.value.toString(),
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.value++,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

We are using just one class – HookExample. There is just one useState hook with an initial value of <int> 0. Every time a user taps the button, the counter’s value is incremented, HookWidget is marked as “dirty” and UI rebuilds. It’s important to mention that the initial value is ignored on subsequent hook calls. This approach requires less code; it’s easy to read and more elegant. Notice that operations on a useState are not done directly on the counter, but counter.value. 

Another super crucial pre-defined hook is useEffect. It takes a function as a first parameter (this is equivalent to an InitState) and gives the option to return another function, which we could use to clean up (just like dispose() method). As the second parameter, it takes a list of “keys”, variables that, when changed, will cause useEffect to execute again. Leaving this array empty will make sure the code runs only once. 

Let’s consider the following example. 

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class UseEffectExample extends HookWidget {
  final _change = false;
  const UseEffectExample({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    useEffect(() {
      debugPrint('onInitState');
      return () => debugPrint('onDisposed');
    }, [_change]);
    useEffect(() {
      debugPrint('onInitState2');
      return () => debugPrint('onDisposed2');
    }, []);
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Hooks Examples'),
        ),
        body: const Center(child: Text('Check your debug console output!')),
      ),
    );
  }
}

Here we have two useEffects, first has value _change assigned to list as a “key”. If this widget is rendered, we have both onInitState and onInitState2 visible in the console. But if we change a value of _change to true and perform a hot reload, only the first hook will be called again. 

The third super popular and handy hook I want to mention is useTextEditingController, which I used in the first example of this article. Default TextEditingController is a thing that is supposed to be created and closed manually. But when using the hook, it calls dispose() automatically, so you don’t have to remember and worry about it! For more super useful hooks, check out the official documentation of the package.

Contact

Would you like to discuss the opportunities of Flutter mobile app development?

Talk to us!

Learn to write more efficient code with Flutter Hooks 

Hooks are a great way to reduce the complexity of your UI code and manage the lifecycle of your widgets. You can choose from many predefined hooks or create your own, which we will discuss in the next article. I strongly recommend checking this excellent tutorial, which covers the situation where the hooks shine the brightest, which is, in my opinion, animations.