Jimmy Aumard
Flutter Community
Published in
5 min readJul 14, 2020

--

Flutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.

Flutter hooks have been available for a while now but they didn't get a lot of love or visibility since then. I am wondering why, because they are awesome!

In this article I'll try to show how you can reduce boilerplate and basically remove all the StatefulWidget you're using today, and also how hooks are easy and cool to use!

First, what are hooks and where do they come from? Anyone?

Okay so they came originally from React (see https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889 to know more about them in a React context), I won't bother you with React as I never used it and probably never will, so it's not mandatory to know about React at all!

Hooks are a way to share the same code with multiple widgets, code that is usually duplicated or hard to share between stateful widgets. The way I describe them is “Hooks are UI logic management”.

I'll present you the hooks I use the most in my apps and their stateful widgets equivalent for you to compare both and see what the gain actually is.

Memoized hook:

This hook is a simple way to cache an instance of an object during the lifecycle of your widget. Pretty handy to create your BLoC, MobX store or notifier objects for your screens.

Here is the stateful widget version:

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final store = MyStore();

_MyHomePageState();

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

And now the hook version:

class MyHomePage extends HookWidget {
@override
Widget build(BuildContext context) {
final store = useMemoized(() => MyStore());
return Container();
}
}

Both examples are doing the same job by just creating an instance of MyStore during the lifetime of the widget.

The gain here is not much, but generally you want to initialize your object to load the data for example, and for that Hooks got you covered too. Let's see useEffect now!

Effect hook:

Like I said we want to load data, and in order to do that we usually call a method on initState.

Stateful widget version:

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final store = MyStore();

_MyHomePageState();
@override
void initState() {
store.loadData();
super.initState();
}
@override
Widget build(BuildContext context) {
return Container();
}
}

Hook version:

class MyHomePage extends HookWidget {
@override
Widget build(BuildContext context) {
final store = useMemoized(() => MyStore());
useEffect(() {
store.loadData();
}, const []);
return Container();
}
}

Applying useEffect simulates initState and will be called only once during the lifetime of the widget. You can also return a function that will be called when the widget is disposed if needed, like this:

useEffect(() {
store.loadData();
return store.dispose;
}, const []);

Looks good right? The const [] means that until the widget is not disposed, don’t call the effect. You can provide an array of parameters and when one changed, the effect will be called.

Let’s see another example with animations!

Animation hooks:

Here is a simple example of how to rotate a box when a button is tapped:

Basic rotate animation with stateful widget

And here is the hook equivalent:

We can see that hooks manage the lifecycle of the controller for us, no need to dispose it, no need to provide the ticker provider like in the stateful widget.

Hooks allow you to create your own hooks, meaning that if you don't find something built-in, just create yours.

Let's see how we can create one to manage a TabController.

Custom hooks:

The flutter_hooks package provides two ways of doing custom hooks, by simply using a function or by creating a custom class.

First, let’s see a custom hook implemented as a function:

TabController useTabController({@required int length, int initialIndex = 0}) {
final tickerProvider = useSingleTickerProvider(keys: [length, initialIndex]);
final controller = useMemoized(() => TabController(length: length, vsync: tickerProvider, initialIndex: initialIndex), [tickerProvider]);

useEffect(() {
return controller.dispose;
}, [controller]);

return controller;
}

Let's decompose this a bit. To create a TabController we need a ticker provider, the number of tabs and an optional initial index for the current tab.

The ticker provider is taken care of by an existing hook called useSingleTickerProvider. That one is easy, both length and initialIndex have to be provided when our custom hook will be used.

You see that a set of keys is passed to useSingleTickerProvider. This is to ensure that the ticker provider is recreated whenever any of those keys is changed. For example, if the number of tabs has changed.

We need to cache TabController to have it once in the widget lifetime, that's why we're using useMemoized. Here we pass the tickerProvider as second parameter in order to recreate the controller if the ticker changes, i.e. when length or initialIndex updates. Again, it's all automatic!

To dispose the TabController, as we’ve seen earlier, we rely on the useEffect() function to return the controller’s dispose method.
Note that the method will also be called if a new TabController is provided as a second parameter.

What about a custom hook class?

Since hook functions are so easy to use, I haven’t needed to implement one as a class, but let’s see how to do it.

Whenever your hook’s complexity grows, you should implement it as a class and in fact, the package’s documentation recommends it.

Here is our TabController hook as a custom class:

TabController useTabController({@required int length, int initialIndex = 0}) {
return use(TabControllerHook(length, initialIndex));
}
class TabControllerHook extends Hook<TabController> {
final int length;
final int initialIndex;

const TabControllerHook(this.length, this.initialIndex);

@override
HookState<TabController, TabControllerHook> createState() {
return _TabControllerHookState();
}
}

class _TabControllerHookState extends HookState<TabController, TabControllerHook> {
@override
build(BuildContext context) {
final tickerProvider = useSingleTickerProvider(keys: [hook.length, hook.initialIndex]);
final controller = useMemoized(() => TabController(length: hook.length, vsync: tickerProvider, initialIndex: hook.initialIndex), [tickerProvider]);

useEffect(() {
return controller.dispose;
}, [controller]);

return controller;
}
}

You can see that a hook works like a stateful widget! You have a state class, a HookState class that has access to the fields of your custom Hook class (here hook.length). And the build method of the hookState build the result of your hook. So still pretty easy to do!

Hooks provide much more than just those shortcuts. For example, it can help you with forms by managing FocusNode or TextEditingController. Jump over to the official documentation to read more about it.

I love hooks and use them in all my projects. I generally couple them with Provider and MobX.
You can find hooks on pub, https://pub.dev/packages/flutter_hooks, and it's very well documented

Let me know in the comments if you would like to know more about hooks or how to use them with other popular packages such as Provider and MobX.

See you later folks!

https://www.twitter.com/FlutterComm

--

--