Skip to content

Widget hooks #25280

@rrousselGit

Description

@rrousselGit

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 duration updating on hot-reload as opposed to creating an AnimationController in the initState .
  • 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-

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: new featureNothing broken; request for a new capabilityframeworkflutter/packages/flutter repository. See also f: labels.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions