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:
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 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 StatefullWidget
s.
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 Stream
s instead of Future
s.
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 useEffect
s 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.
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.
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!