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
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.
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:
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.
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
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.
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.
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.
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
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.
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.
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
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.
We’ve already covered this hook – it manages the widget’s
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.
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.
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
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.
We can have parameters and their default values there, just like in any other function. The function just calls the
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
Then we must create a
Hook and its
HookState classes and implement the required logic.
HookState works just like
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.
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
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
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.
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!