Hook into Flutter Hooks
Hook into Flutter Hooks
Hook into Flutter Hooks

Hook into Flutter Hooks

Flutter

Flutter

Flutter

Does boilerplate code in Flutter bother you? We’ll Hook you up.

26. 4. 2023

When developing basic features for apps, such as animations, scrolling, tabs, pages, or forms, it's common to have a bit of excess code. However, this state of overabundance can make developing new features or identifying bugs difficult. To address this issue, we’ve had to come up with a clever solution that would help us fulfill these requirements:

  • maintain current functionality while making improvements,

  • increase coding efficiency without compromising quality,

  • simplify the codebase by removing or abstracting boilerplate code to improve readability.

We looked around and found the flutter_hooks package. It looked promising from the get go, so we decided to give it a shot!

To our great satisfaction, it easily meets all our requirements. And we can even extend its functionality and create our work-simplifying hooks.

Never heard of hooks? Let us hook you into flutter_hooks.

The problem

Widgets in Flutter can easily become very large even with good practices. One might separate widgets into smaller ones, but that can be a strenuous process. Take controllers, for instance. Frequently, widgets have to use multiple of them: a controller for scrolling, tabs, one, two, three, or even more animation controllers. All that adds additional boilerplate to our widgets that are bland in all the wrong ways.

Although boilerplate code is meant to be reused with minor modifications, using it repeatedly across the codebase makes it challenging to modify, and you’ll likely waste time searching for necessary code. No matter how much you think copy-paste is automatic, it’s the perfect place for an error, like when you forget to change its last occurrence. Not to mention you must copy-paste several parts of code each time. Forgetting one thing always leads to multiple issues.

Reducing boilerplate

In Flutter, when creating controllers, subscribing to listenable, using streams, or managing side effects, the code is rarely as minimal as it could be. Let’s see the example from the flutter_hooks documentation.

Be sure to read it carefully and ask yourself what is essential and what the boilerplate is:

class Example extends StatefulWidget {
  final Duration duration;

  const Example({Key? key, required this.duration})
      : super(key: key);

  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
  AnimationController? _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
  }

  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.duration != oldWidget.duration) {
      _controller!.duration = widget.duration;
    }
  }

  @override
  void dispose() {
    _controller!.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Yes, you got it.

The only part that matters is the build method, the duration variable, and the class itself, which could even be stateless. However, due to the need for AnimationController, the widget has to be stateful. It must use the SingleTickerProviderStateMixin mixin so the animation controller can call animation updates per frame. It also needs to be initialized, disposed, and updated. We even have to ensure that we use TickerProviderStateMixin once we use multiple animation controllers or other code requiring a Ticker. But that’s impossible to achieve in cases where you also need to use a mixin – it can only be used once on a class. It also shares all properties with it and its ancestors, which is complicated in its own right.

That’s where the flutter_hooks come into play.

First, let us peek at the code we can write now. Below is an example of code equal to the first one above. Easier to read, isn’t it? Notice how only the parts we previously marked as important stayed while only one additional line of code was added.

class Example extends HookWidget {
  const Example({Key? key, required this.duration})
      : super(key: key);

  final Duration duration;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: duration);
    return Container();
  }
}

A closer look at hooks

First, to grasp the basics of how hooks work, let us recap how Flutter works in a nutshell.

In general, Flutter works with three trees:

The first one is the widget tree. This immutable tree holds the configuration for UI, and we can interact with it through a public API. The second one is a mutable render-object tree. This tree knows all about widgets' layout, size, composition, and other things. And the third one is a mutable element tree. This tree connects the two, manages the trees, and represents the UI.

Because the widget tree is immutable, we cannot store anything because everything will reset on the next build, similar to the State in StatefulWidget. Flutter does that by storing a state inside an element. Similarly, this package keeps a List<Hook>. To do that, you must use HookWidget, which creates HookElement underneath. Alternatively, StatefulHookWidget if you feel the need to use State, so you can, for example, mutate the data using setState or use life cycle methods like initState. All hooks use a notation to prefix use*, so it's easy to know if the function is or isn't a hook.

Seeing the improved code, you might have asked if there are multiple hooks you can store in an element; how does it know which one to use if you don’t set up some ids? It’s quite simple. It just returns hooks by the order they’re called.

Similarly, all hooks can react to all life cycle events widgets can. This way, initialization, destruction, and other events can be handled simultaneously in one place, significantly reducing a boilerplate. For example, the animation controller hook creates and destroys the controller for us.

Things to look out for

Because of its properties described, especially how hooks are looked up and connected, some issues might arise while you use hooks.

It all comes to one: call use methods for all hooks in the same order on every build. That implies you will not use conditions or nest them in callbacks. I think the best and the least error-prone way is to call every hook at the beginning of the build method. And only in the build method. Don’t try to call it in callbacks, other methods, nested in conditions, or nested functions. It might work at first, but you will probably face an error sooner or later.

The last thing to cover is the hot reload, aka The Most Awesome Flutter Feature.

Hooks work with hot reload, but you have to remember how it looks for hooks. As was said, it looks for them in order, meaning if you change the order, delete, or add any, and then hot reload, it might not work as expected. If you append a hook, it will work. If you prepend a hook, hooks that follow the new one will reset. If the types don’t match, it will reset itself. If hooks know what to give you, it will work as you want.

Existing hooks

You can use a great collection of hooks in the flutter_hooks package. Or you can always find some more hooks in other packages on Pub. We will cover hooks we use most often because they give us the biggest benefit.

useState

Managing the state of the app state is without discussion. You can use a library like Bloc that helps you separate it from the UI. Ephemeral state, also called UI state, is more benevolent, and you can do plenty with it. The standard is to convert StatelessWidget into a StatefullWidget where we can manage its State. That’s ok, but again, it forces us to separate a stateless class into two, a stateful class and a state class, which also adds a bit of a boilerplate, especially when you only need one boolean for your widget’s state.

Using flutter_hook’s useState we can encapsulate that so we can still use StatelessWidget while having a state cool. For more complex widgets with many UI states, I would still advise using StatefullWidgets.

Another cool feature is that the widget is marked for rebuilding when the state changes. That mimics the behavior of setState callback, so if you need to have a state with rebuilds, useState is the right choice.

useMemoized, useFuture, useStream

Another handy hook is useMemoized. This hook caches an instance to an object provided in a builder callback invoked only in the first run. This function is very handy in cases when you do not want to recompute a value. Like in combination with useFuture, which has a similarly bad effect as FutureBuilder — basically, for each rebuild, a future callback is called again and again.

Hook useFuture subscribes to a Future value. Returns a AsyncSnapshot, that represents the current state of a snapshot. There is also the useStream hook that works in a very similar manner, just for Streams instead of Futures.

useTabController, useScrollController, usePageController

We often use hooks for scrolling, tab views, and page views. It is pretty simple to initialize and configure hooks while minimizing a boilerplate and focusing on what truly matters. Hooks manage the whole life cycle; the specific controller is automatically disposed on the widget’s disposal life-cycle event.

useAnimationController

We’ve already covered this hook – it manages the widget’s initState, didUpdateWidget, and dispose of life-cycle methods and also manages the correct usage of SingleTickerProviderStateMixin by internally using the useSingleTickerProvider hook; a hook used, as you might have guessed, for providing a Ticker to the widget.

useEffect

When you need side effects, you can use the useEffect function to do it. You may use the hook’s keys often because they control when, and if the function is called only when keys have changed since the last time, the effect callback is executed. So if you set keys to empty list [], it will be executed only once. You can also return a function from the callback, which will be used at the widget’s disposal. That can be pretty handy to close streams, for instance.

Since you use useEffects in the build method, I suggest using StatefulHookWidget when you need to use more effects, for example, only on init. In StatefulHookWidget , you can use initHook or other life-cycle methods for it. Having similar effects in the build method adds more boilerplate, and you should keep this method as small as possible.

other

That is far from all. Check other hooks that come with flutter_hooks.

Create your hook

As previously mentioned, you can create your own hook to help your needs. It’s quite simple. We created a hook for FixedExtentScrollController, just to name one. To follow flutter_hooks conventions, you should create your hook within a use* method.

Simple hooks can only wrap other hooks and, therefore, can be made using functions. We have a couple of rather complex hooks that’ll help you can benefit from life-cycle events. To show an example, let’s look at the hook for FixedExtentScrollController. First, we must create the hook function useFixedExtentScrollController, to follow good practices.

FixedExtentScrollController useFixedExtentScrollController({
  int initialItem = 0,
  bool keepScrollOffset = true,
  List<Object?>? keys,
}) {

We can have parameters and their default values there, just like in any other function. The function just calls the flutter_hook's use function that registers a hook into an element and returns its value. The special parameter here is keys. You should consider if you need them in your hook. We’ve already mentioned keys when talking about useEffect.

Then we must create a Hook and its HookState classes and implement the required logic. HookState works just like State from StatefulWidget, providing the life-cycle methods you can implement for your needs. One exception is the build method, which returns the value of a hook, not a widget. There are also some debug properties and methods to handle widget rebuilds, but let's skip that for now.

class _ScrollControllerHook extends Hook<FixedExtentScrollController> {
  final int initialItem;

  const _ScrollControllerHook({
    required this.initialItem,
    super.keys,
  });

  @override
  HookState<FixedExtentScrollController, Hook<FixedExtentScrollController>> createState() =>
      _ScrollControllerHookState();
}

class _ScrollControllerHookState extends HookState<FixedExtentScrollController, _ScrollControllerHook> {
  late final controller = FixedExtentScrollController(
    initialItem: hook.initialItem,
  );

  @override
  FixedExtentScrollController build(BuildContext context) => controller;

  @override
  void dispose() => controller.dispose();

  @override
  String get debugLabel => 'useFixedExtentScrollController';
}

As you can see, we created the Hook and accompanying HookState classes. Note that the controller property has to use the late keyword. That’s because it uses hook, which is set after the createState method of Hook is called. To access it and use it to init something, you must call it after initialization. Therefore, use late.

Another option would be to use the initHook method, but that would mean having a nullable type. Better to use late here. But you probably know all of that from regular State's widget getter. To have some more info in the DevTools, override debugLabel. And that’s it. You have just created your first hook. Pretty easy, right? Now imagine if you could extract much more stuff from your State or a build method than creating a controller and disposing of it.

Closing thoughts

While it may seem strange at first glance, using hooks is not considered a bad practice or cheating. Hooks extend elements of widgets and grant you additional superpowers. They are a handy tool that doesn’t reduce widget efficiency but eliminates a lot of boilerplate.

Check them out, use them if you find them interesting, and let us know if you think of other hooks that might come in handy.

We’d be happy to cooperate with you!

Further readings

flutter_hooks | pub.dev