flutter_hooks 0.6.1

  • Readme
  • Changelog
  • Example
  • Installing
  • 95

Build Status codecov pub package pub package

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 manages a Widget life-cycles. They exist for one reason: increase the code sharing between widgets and as a complete replacement for StatefulWidget.

Motivation #

StatefulWidget suffer 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 {
  final Duration duration;

  const Example({Key key, @required this.duration})
      : assert(duration != null),
        super(key: key);

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

class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
  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 from scratch, which is of course undesired.

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

  • A given mixin can only be used once per class.
  • Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior.

This library propose a third solution:

class Example extends HookWidget {
  final Duration duration;

  const Example({Key key, @required this.duration})
      : assert(duration != null),
        super(key: key);

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

This code is strictly 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 moved into useAnimationController, a function included directly in this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). It is what we call a Hook.

Hooks are a new kind of objects with some specificities:

  • They can only be used in the build method of a HookWidget.
  • The same hook is reusable an infinite number of times The following code defines two independent AnimationController, and they are correctly preserved when the widget rebuild.
Widget build(BuildContext context) {
  final controller = useAnimationController();
  final controller2 = useAnimationController();
  return Container();
}
  • Hooks are entirely independent of each other and from the widget. Which means they can easily be extracted into a package and published on pub for others to use.

Principle #

Similarly to State, hooks are stored on the Element of a Widget. But instead of having one State, the Element stores a List<Hook>. Then 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 returns the third hook, ...

If this is still unclear, a naive implementation of hooks is the following:

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 they are implemented, here's a great article about how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

Rules #

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

DO call use unconditionally

Widget build(BuildContext context) {
  Hook.use(MyHook());
  // ....
}

DON'T wrap use into a condition

Widget build(BuildContext context) {
  if (condition) {
    Hook.use(MyHook());
  }
  // ....
}

DO always call all the hooks: #

Widget build(BuildContext context) {
  Hook.use(Hook1());
  Hook.use(Hook2());
  // ....
}

DON'T aborts build method before all hooks have been called:

Widget build(BuildContext context) {
  Hook.use(Hook1());
  if (condition) {
    return Container();
  }
  Hook.use(Hook2());
  // ....
}

About hot-reload #

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

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

Consider the following list of hooks:

Hook.use(HookA());
Hook.use(HookB(0));
Hook.use(HookC(0));

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

Hook.use(HookA());
Hook.use(HookB(42));
Hook.use(HookC());

Here everything works fine; all hooks keep their states.

Now consider that we removed HookB. We now have:

Hook.use(HookA());
Hook.use(HookC());

In this situation, HookA keeps its state but HookC gets a hard reset. This happens because when a refactoring is done, all hooks after the first line impacted are disposed. Since HookC was placed after HookB, is got disposed.

How to use #

There are two ways to create a hook:

  • A function

Functions are by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention, these functions will be prefixed by use.

The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes:

ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
  final result = useState<T>(initialData);
  useValueChanged(result.value, (_, __) {
    print(result.value);
  });
  return result;
}
  • A class

When a hook becomes too complex, it is possible to convert it into a class that extends Hook, which can then be used using Hook.use. As a class, the hook will look very similar to a State and have access to life-cycles and methods such as initHook, dispose and setState. It is usually a good practice to hide the class under a function as such:

Result useMyHook(BuildContext context) {
  return Hook.use(_MyHook());
}

The following defines a hook that prints the time a State has been alive.

class _TimeAlive<T> extends Hook<void> {
  const _TimeAlive();

  @override
  _TimeAliveState<T> createState() => _TimeAliveState<T>();
}

class _TimeAliveState<T> extends HookState<void, _TimeAlive<T>> {
  DateTime start;

  @override
  void initHook() {
    super.initHook();
    start = DateTime.now();
  }

  @override
  void build(BuildContext context) {
    // this hook doesn't create anything nor uses other hooks
  }

  @override
  void dispose() {
    print(DateTime.now().difference(start));
    super.dispose();
  }
}

Existing hooks #

Flutter_hooks comes with a list of reusable hooks already provided.

They are divided in different kinds:

Primitives #

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

namedescription
useEffectUseful for side-effects and optionally canceling them.
useStateCreate variable and subscribes to it.
useMemoizedCache the instance of a complex object.
useContextObtain the BuildContext of the building HookWidget.
useValueChangedWatches a value and calls a callback whenever the value changed.

Object binding #

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

namedescription
useStreamSubscribes to a Stream and return its current state in an AsyncSnapshot.
useStreamControllerCreates a StreamController automatically disposed.
useFutureSubscribes to a Future and return its current state in an AsyncSnapshot.
namedescription
useSingleTickerProviderCreates a single usage TickerProvider.
useAnimationControllerCreates an AnimationController automatically disposed.
useAnimationSubscribes to an Animation and return its value.
namedescription
useListenableSubscribes to a Listenable and mark the widget as needing build whenever the listener is called.
useValueNotifierCreates a ValueNotifier automatically disposed.
useValueListenableSubscribes to a ValueListenable and return its value.

Misc #

A series of hooks with no particular theme.

namedescription
useReducerAn alternative to useState for more complex states.
usePreviousReturns the previous argument called to [usePrevious].

0.6.1: #

  • Added useReassemble hook, thanks to @SahandAkbarzadeh

0.6.0: #

  • Make hooks compatible with newer flutter stable version 1.7.8-hotfix.2.

0.4.0: #

0.3.0: #

  • NEW: usePrevious, a hook that returns the previous argument it received.
  • NEW: it is now impossible to call inheritFromWidgetOfExactType inside initHook of hooks. This forces authors to handle values updates.
  • FIX: use List

example/README.md

Flutter Hooks Gallery #

A series of examples demonstrating how to use Flutter Hooks! It teaches how to use the Widgets and hooks that are provided by this library, as well examples demonstrating how to write custom hooks.

Run the app #

  1. Open a terminal
  2. Navigate to this example directory
  3. Run flutter create .
  4. Run flutter run from your Terminal, or launch the project from your IDE!

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_hooks: ^0.6.1

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:flutter_hooks/flutter_hooks.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
90
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
95
Learn more about scoring.

We analyzed this package on Oct 16, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.5.1
  • pana: 0.12.21
  • Flutter: 1.9.1+hotfix.4

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.7
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
mockito >=4.0.0 <5.0.0
pedantic ^1.4.0