Hooks are a new type of object that manage the life cycles of a widget. They exist for a single purpose: to reduce code duplication across widgets.
StatefulWidget has a major flaw: it’s extremely difficult to reuse the logic. You will frequently wind up with a lot of code in the lifecycle functions, such as initState(). Let’s take AnimationController for example. In order to use an animation controller, you need to mixin a TickerProvider, initialize the animation controller in the initState and then dispose as shown.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class _AnimationControllerExampleState extends State < AnimationControllerExample > with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChildWidget();
}
}
All widgets that want to utilize an AnimationController will have to re-implement practically everything from the ground up, which is obviously undesirable. With Flutter hooks, we can have;
1
2
3
4
5
6
7
8
class HookAnimationControllerExample extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1));
return Container();
}
}
All the animation controller logic is now moved into useAnimationController. This is what is referred to as a hook.
They can only be utilized in the build method of a widget that mix-in hooks
The same hook can be reused indefinitely, and they are preserved correctly when the widget rebuilds
1
2
3
4
5
6
Widget build(BuildContext context) {
final controller1 = useAnimationController(duration: Duration(seconds: 1));
final controller2 = useAnimationController(duration: Duration(seconds: 2));
final controller3 = useAnimationController(duration: Duration(seconds: 3));
return Container();
}
They are entirely independent of each other and also from the widget
useEffect takes an effect callback and calls it synchronously,
1
2
3
4
5
6
7
8
9
10
11
class UseEffectExample extends HookWidget {
final Stream errorStream;
@override
Widget build(BuildContext context) {
useEffect(() {
final errorMsgSubscription = errorStream.listen(print);
return errorMsgSubscription.cancel;
});
return Container();
}
}
Creates and subscribes to a variable.
1
2
3
4
5
6
7
8
9
10
11
12
class CounterWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final count = useState(0);
return GestureDetector(
// Rebuilds the Counter widget automatically.
onTap: () => count.value++,
child: Text(count.value.toString()),
);
}
}
For an exhaustive list check here.
Flutter hooks provide two methods for creating custom hooks: a function and a class.
There are two principles to follow while creating custom hooks:
Always prefix your hooks with “use” to indicate that this is a hook function
Hooks should not be rendered conditionally, i.e, do not wrap a hook function in a conditional statement as follows:
1 2 3 4 5 6
Widget build(BuildContext context) { if (condition) { useAHook(); } // .... }
To get started with the function method, we’ll need to create a method that uses one of the built-in hooks.
For instance, we can have a hook method that fades after another animation is completed as shown below.
1 2 3 4 5 6 7 8 9
Animation < double > useFadeTransition(AnimationController animationController) { final _offsetAnimation = useAnimationController(duration: const Duration()); animationController.addListener(() { if (animationController.isCompleted) { _offsetAnimation.forward(); } }); return _offsetAnimation; }
We can also have another hook method that changes location in response to another animation as shown below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animation < Offset > useOffsetController(AnimationController animationController) {
const _startOffset = Offset(0.0, 3.0);
const _endOffset = Offset(0.0, 0.0);
final _offsetAnimation =
useAnimationController(duration: const Duration(milliseconds: 500));
animationController.addListener(() {
if (animationController.isCompleted) {
_offsetAnimation.forward();
}
});
return Tween < Offset > (begin: _startOffset, end: _endOffset)
.animate(
CurvedAnimation(parent: _offsetAnimation, curve: Curves.easeInSine));
}
Custom hooks are used in similar ways to inbuilt hooks. They can be used in a hook widget as shown below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CustomHookExample extends HookWidget {
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(milliseconds: 1000),
lowerBound: 0.0,
upperBound: 1.0).
.forward();
final offsetAnimationController = useOffsetController(animationController);
final fadeAnimationController = useFadeTransition(animationController);
return Scaffold(
body: Center(
child: FadeTransition(
opacity: fadeAnimationController,
child: SlideTransition(
position: offsetAnimationController, child: ChildWidget()),
),
),
);
}
}
Functions are elegant, functions are wonderful, but hooks created as basic functions cannot possibly know about the underlying widget’s lifecycle. This means that you must create a full-fledged hook class anytime you instantiate something that needs to know about the life cycle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class FadeTransitionHook extends Hook < Animation < double >> {
final AnimationController animationController;
FadeTransitionHook(this.animationController);
@override
HookState < Animation < double > ,
Hook < Animation < double >>> createState() => _FadeTransitionHookState();
}
class _FadeTransitionHookState extends HookState < Animation < double > , FadeTransitionHook > {
final _offsetAnimation = useAnimationController(duration: const Duration());
@override
void initHook() {
super.initHook();
hook.animationController.addListener(() {
if (hook.animationController.isCompleted) {
_offsetAnimation.forward();
}
});
}
@override
Animation < double > build(BuildContext context) {
return _offsetAnimation;
}
@override
void dispose() {
_offsetAnimation.dispose();
super.dispose();
}
}
The hook code is organized almost identically to that of a StatefulWidget, and we have access to the dispose method.
Finally, we need to register the hook with a HookWidget using the Hook.use method.
1 2 3 4 5
Animation < double > useFadeTransition( AnimationController animationController, ) { return Hook.use(FadeTransitionHook(animationController, )); }
The usage of this is very similar to that of the function method.
Flutter hooks revolutionized the way we design Flutter widgets by allowing us to reduce the codebase to a fraction of its original size.
As we’ve seen, Flutter hooks allow developers to avoid using widgets like StatefulWidget and instead write clear, maintainable code that’s simple to distribute and test.