-
Notifications
You must be signed in to change notification settings - Fork 30.2k
Widget hooks #25280
Description
React team recently announced hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889. Due to how similar Flutter is with React, it would probably be interesting to see if these fits to Flutter too.
The definition:
Hooks are very similar to the State of StatefulWidget with one main difference: We can have as many hooks as we like on a widget.
Hooks have access to all the life-cycles that a State have (or a modified version).
Hooks can be used on any given widget. Contrarily to State which can be used only for one specific widget type.
Hooks are different from super mixins because they cannot generate conflicts. Hooks are entirely independent and unrelated to the widget.
This means that a Hook can be used to store values and publicly expose it without fear of conflicts. It also means that we can reuse the same Hook multiple times, contrarily to mixins.
The principle:
Hooks are basically stored within the Element in an Array. They are accessible only from within the build method of a widget. And hooks should be accessed unconditionally, example:
DO:
Widget build(BuildContext context) {
Hook.use(MyHook());
}DON'T:
Widget build(BuildContext context) {
if (condition) {
Hook.use(MyHook());
}
}This restriction may seem very limiting, but it is because hooks are stored by their index. Not their type nor name.
This allows reusing the same hook as many time as desired, without any collision.
The use case
The most useful part of Hooks is that they allow extracting life-cycle logic into a reusable component.
One typical issue with Flutter widgets is disposable objects such as AnimationController.
They usually require both an initState and a dispose override. But at the same time cannot be extracted into a mixin for maintainability reasons.
This leads to a common code-snipper: stanim
class Example extends StatefulWidget {
@override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
);
}
}Hooks solves this issue by extracting the life-cycle logic. This leads to a potentially much smaller code:
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
AnimationController controller = useAnimationController(duration: const Duration(seconds: 1));
return Container(
);
}
}A naive implementation of such hook could be the following function:
AnimationController useAnimationController({Duration duration}) {
// use another hook to obtain a TickerProvider
final tickerProvider = useTickerProvider();
// create an AnimationController once
final animationController = useMemoized<AnimationController>(
() => AnimationController(vsync: tickerProvider, duration: duration)
);
// register `dispose` method to be closed on widget removal
useEffect(() => animationController.dispose, [animationController]),
// synchronize the arguments
useValueChanged(duration, (_, __) {
animationController.duration = duration;
});
return animationController;
}useAnimationController is one of those "Hook".
Where a naive implementation of such hook would be the following:
That code should look similar to somebody used to State class. But this has a few interesting points:
- The hook entirely takes care of the
AnimationController, from creation to disposal. But also updates. - It can easily be extracted into a reusable package
- It also takes care of
durationupdating on hot-reload as opposed to creating anAnimationControllerin theinitState. - Hooks can use other hooks to build more complex logic
Drawbacks
Hooks comes with a cost. The access to a value has an overhead similar to an InheritedElement. And they also require to create a new set of short-lived objects such as closures or "Widget" like.
I have yet to run a benchmark to see the real difference though
Another issue is about hot-reload.
Adding hooks at the end of the list if fine. But since hooks work based on their order, adding hooks in the middle of existing hooks will cause a partial state reset. Example:
Going from A, B to A, C, B will reset the state of B (calling both dispose and initHook again).
Conclusion
Hooks simplifies a lot the widgets world by allowing a bigger code reuse. Especially on the very common scenarios such as dispose, memoize and watching a value.
They can be used to entirely replace a StatefulWidget.
But they require a mind shift though, and the partial state reset on refactoring may be bothersome.
It is possible to extract hooks outside of Flutter by creating custom Elements. There's no need to modify the sources as of now.
But due to the specificity of hooks, it would benefit a lot from a custom linter. Which external packages cannot provide at the moment.
Bonus
A work in progress implementation is available here: https://github.com/rrousselGit/flutter_hooks (latest features are on prod-ready branch).
A release is planned soon as alpha. But the current implementation works to some extents.
Available here https://pub.dartlang.org/packages/flutter_hooks#-readme-tab-