Home

Awesome

English | Português | 한국어 | 简体中文

Build codecov pub package pub package <a href="https://discord.gg/Bbumvej"><img src="https://img.shields.io/discord/765557403865186374.svg?logo=discord&color=blue" alt="Discord"></a>

<img src="https://raw.githubusercontent.com/rrousselGit/flutter_hooks/master/packages/flutter_hooks/flutter-hook.svg?sanitize=true" width="200">

Flutter Hooks

A Flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889

Hooks are a new kind of object that manage the life-cycle of a Widget. They exist for one reason: increase the code-sharing between widgets by removing duplicates.

Motivation

StatefulWidget suffers from a big problem: it is very difficult to reuse the logic of say initState or dispose. An obvious example is AnimationController:

class Example extends StatefulWidget {
  const Example({super.key, required this.duration});

  final Duration duration;

  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
  }

  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.duration != oldWidget.duration) {
      _controller.duration = widget.duration;
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

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

All widgets that desire to use an AnimationController will have to reimplement almost all of this logic from scratch, which is of course undesired.

Dart mixins can partially solve this issue, but they suffer from other problems:


This library proposes a third solution:

class Example extends HookWidget {
  const Example({super.key, required this.duration});

  final Duration duration;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: duration);
    return Container();
  }
}

This code is functionally equivalent to the previous example. It still disposes the AnimationController and still updates its duration when Example.duration changes. But you're probably thinking:

Where did all the logic go?

That logic has been moved into useAnimationController, a function included directly in this library (see Existing hooks) - It is what we call a Hook.

Hooks are a new kind of object with some specificities:

Principle

Similar to State, hooks are stored in the Element of a Widget. However, instead of having one State, the Element stores a List<Hook>. Then in order to use a Hook, one must call Hook.use.

The hook returned by use is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third call returns the third hook and so on.

If this idea is still unclear, a naive implementation of hooks could look as follows:

class HookElement extends Element {
  List<HookState> _hooks;
  int _hookIndex;

  T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this);

  @override
  performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
  }
}

For more explanation of how hooks are implemented, here's a great article about how it was done in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

Rules

Due to hooks being obtained from their index, some rules must be respected:

DO always prefix your hooks with use:

Widget build(BuildContext context) {
  // starts with `use`, good name
  useMyHook();
  // doesn't start with `use`, could confuse people into thinking that this isn't a hook
  myHook();
  // ....
}

DO call hooks unconditionally

Widget build(BuildContext context) {
  useMyHook();
  // ....
}

DON'T wrap use into a condition

Widget build(BuildContext context) {
  if (condition) {
    useMyHook();
  }
  // ....
}

About hot-reload

Since hooks are obtained from their index, one may think that hot-reloads while refactoring will break the application.

But worry not, a HookWidget overrides the default hot-reload behavior to work with hooks. Still, there are some situations in which the state of a Hook may be reset.

Consider the following list of hooks:

useA();
useB(0);
useC();

Then consider that we edited the parameter of HookB after performing a hot-reload:

useA();
useB(42);
useC();

Here everything works fine and all hooks maintain their state.

Now consider that we removed HookB. We now have:

useA();
useC();

In this situation, HookA maintains its state but HookC gets hard reset. This happens because, when a hot-reload is performed after refactoring, all hooks after the first line impacted are disposed of. So, since HookC was placed after HookB, it will be disposed.

How to create a hook

There are two ways to create a hook:

Existing hooks

Flutter_Hooks already comes with a list of reusable hooks which are divided into different kinds:

Primitives

A set of low-level hooks that interact with the different life-cycles of a widget

NameDescription
useEffectUseful for side-effects and optionally canceling them.
useStateCreates a variable and subscribes to it.
useMemoizedCaches the instance of a complex object.
useRefCreates an object that contains a single mutable property.
useCallbackCaches a function instance.
useContextObtains the BuildContext of the building HookWidget.
useValueChangedWatches a value and triggers a callback whenever its value changed.

Object-binding

This category of hooks the manipulation of existing Flutter/Dart objects with hooks. They will take care of creating/updating/disposing an object.

dart:async related hooks:

NameDescription
useStreamSubscribes to a Stream and returns its current state as an AsyncSnapshot.
useStreamControllerCreates a StreamController which will automatically be disposed.
useOnStreamChangeSubscribes to a Stream, registers handlers, and returns the StreamSubscription.
useFutureSubscribes to a Future and returns its current state as an AsyncSnapshot.

Animation related hooks:

NameDescription
useSingleTickerProviderCreates a single usage TickerProvider.
useAnimationControllerCreates an AnimationController which will be automatically disposed.
useAnimationSubscribes to an Animation and returns its value.

Listenable related hooks:

NameDescription
useListenableSubscribes to a Listenable and marks the widget as needing build whenever the listener is called.
useListenableSelectorSimilar to useListenable, but allows filtering UI rebuilds
useValueNotifierCreates a ValueNotifier which will be automatically disposed.
useValueListenableSubscribes to a ValueListenable and return its value.
useOnListenableChangeAdds a given listener callback to a Listenable which will be automatically removed.

Misc hooks:

A series of hooks with no particular theme.

NameDescription
useReducerAn alternative to useState for more complex states.
usePreviousReturns the previous argument called to [usePrevious].
useTextEditingControllerCreates a TextEditingController.
useFocusNodeCreates a FocusNode.
useTabControllerCreates and disposes a TabController.
useScrollControllerCreates and disposes a ScrollController.
usePageControllerCreates and disposes a PageController.
useFixedExtentScrollControllerCreates and disposes a FixedExtentScrollController.
useAppLifecycleStateReturns the current AppLifecycleState and rebuilds the widget on change.
useOnAppLifecycleStateChangeListens to AppLifecycleState changes and triggers a callback on change.
useTransformationControllerCreates and disposes a TransformationController.
useIsMountedAn equivalent to State.mounted for hooks.
useAutomaticKeepAliveAn equivalent to the AutomaticKeepAlive widget for hooks.
useOnPlatformBrightnessChangeListens to platform Brightness changes and triggers a callback on change.
useSearchControllerCreates and disposes a SearchController.
useWidgetStatesControllerCreates and disposes a WidgetStatesController.
useExpansionTileControllerCreates a ExpansionTileController.
useDebouncedReturns a debounced version of the provided value, triggering widget updates accordingly after a specified timeout duration
useDraggableScrollableControllerCreates a DraggableScrollableController.

Contributions

Contributions are welcomed!

If you feel that a hook is missing, feel free to open a pull-request.

For a custom-hook to be merged, you will need to do the following:

Sponsors

<p align="center"> <a href="https://raw.githubusercontent.com/rrousselGit/freezed/master/sponsorkit/sponsors.svg"> <img src='https://raw.githubusercontent.com/rrousselGit/freezed/master/sponsorkit/sponsors.svg'/> </a> </p>