Jul 19, 2021, Mobile

Flutter Hooks and how to hook them up

Robert Sobczyk Flutter developer
image

Flutter Hooks is an implementation of React hooks that provide a robust and simple way to manage Widget life-cycle by increasing code sharing and reducing duplication. Hooks original source comes from React where they are popular and were adapted by the community. And due to the fact that React and Flutter are declarative languages, the solution works well as a substitute for Flutter’s StatefulWidget.

The flutter_hooks package was prepared by Remi Rousselet, the creator of the popular Provider package.

⚡ TL;DR:Hooks are used to share the same code which is duplicated or not easy to share between stateful widgets. It also allows you to reduce a lot of boilerplate code and respect the single responsibility principle.

Difference in the implementation of the sample Flutter application that counts clicks, using StatefulWidget and HookWidget:

Some rules

🚫

Each widget build must call the same hooks in the same order which means that you cannot call them after conditionally exiting the build nor in an “if” statement.

Hooks must be used in the body of the build method.

Try to use the “use” prefix for your Hooks and call them at the start of the build. You will refer to their values afterwards.

Some info

Hooks solve the biggest StatefulWidget problem: it is hard to reuse logic from initState, dispose, didUpdateWidget and other widget lifecycle methods. For every similar widget, we must reimplement code from scratch in a non-elegant way. This can be partially solved with a mixins, but this solution has its limitations: they can be used once per class. Also, both mixins and the class share the same object that in some circumstances may generate some kind of unknown behavior.

Hooks solve this problem by moving all boilerplate logic to its own body that can be used only in the build method and be reused an infinite amount of times. 

Sample use cases hooks:

useState

Simplest hook that creates a variable and subscribes to it.

The returned object is of the ValueNotifier<T> type, therefore it is not accessed directly through the variable, but through the variable.value. When the state changes, the widget rebuilds itself, as does the default setState.

useMemoized      

Hook that caches the instance of a complex object.

This hook allows you to create an Object (such as a Stream or Future) the first time this builder function is invoked, without recreating it on each subsequent build – like initState but without it. That example also shows the use of the useStream hook. It serves to listen for updates to the Stream. This triggers a rebuild whenever a new value is emitted.

useEffect     

Hook useful for side effects and optionally canceling them.

This hook is called synchronously on every build, unless keys are specified. In this case, useEffect is called again only if any value inside the keys has changed. It also takes an effective callback and calls it synchronously. This effect can return a function that will be called when the widget is disposed of or when the effect is called again. Unless keys are specified, effect is called on every build. In another case it is called once on the first call or on a key’s change.

Use custom hooks     

There are two ways to deliver custom hooks – as a function or as a custom class:

Function is the most common way to write hooks. Since the hooks are inherently composed, the function will be able to combine other hooks to create a custom one.

Example of using TabController as function:

Due to the fact that hooks can get more complicated, it is possible to convert them into a class. Hook as a class is very similar in looks to a State. Also, hook lifecycle methods are congruous to those known from the State.

It is good practice to wrap call of class hook to method like useTabController

Example of useTabController as class:

To handle the TabController, we must use a ticker provider which we will deliver by useSingleTickerProvider hook which takes ticker provider arguments: length and initialIndex as keys. That ensures that the provider will be recreated on any key change.

We use useMemoized to cache TabController to have it once in the widget lifetime. However, we pass tickerProvider as a key to ensure that the controller will recreate when tickerProvider changes.

Disposing TabController depends on the useEffect hook that disposes the controller when needed or when a new TabController has been provided via the parameter

Share