Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork186
React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget.
License
rrousselGit/flutter_hooks
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
English |Português |한국어 |简体中文 |日本語
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 aWidget
. They existfor one reason: increase the code-sharingbetween widgets by removing duplicates.
StatefulWidget
suffers from a big problem: it is very difficult to reuse thelogic of sayinitState
ordispose
. An obvious example isAnimationController
:
classExampleextendsStatefulWidget {constExample({super.key,requiredthis.duration});finalDuration duration;@override_ExampleStatecreateState()=>_ExampleState();}class_ExampleStateextendsState<Example>withSingleTickerProviderStateMixin {latefinalAnimationController _controller;@overridevoidinitState() {super.initState(); _controller=AnimationController(vsync:this, duration: widget.duration); }@overridevoiddidUpdateWidget(Example oldWidget) {super.didUpdateWidget(oldWidget);if (widget.duration!= oldWidget.duration) { _controller.duration= widget.duration; } }@overridevoiddispose() { _controller.dispose();super.dispose(); }@overrideWidgetbuild(BuildContext context) {returnContainer(); }}
All widgets that desire to use anAnimationController
will have to reimplementalmost all of this logic 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 share the same object.
This means that if two mixins define a variable under the same name, the resultmay vary between compilation fails to unknown behavior.
This library proposes a third solution:
classExampleextendsHookWidget {constExample({super.key,requiredthis.duration});finalDuration duration;@overrideWidgetbuild(BuildContext context) {final controller=useAnimationController(duration: duration);returnContainer(); }}
This code is functionally equivalent to the previous example. It still disposes theAnimationController
and still updates itsduration
whenExample.duration
changes.But you're probably thinking:
Where did all the logic go?
That logic has been moved intouseAnimationController
, a function included directly inthis library (seeExisting hooks) - It is what we call aHook.
Hooks are a new kind of object with some specificities:
They can only be used in the
build
method of a widget that mix-inHooks
.The same hook can be reused arbitrarily many times.The following code defines two independent
AnimationController
, and they arecorrectly preserved when the widget rebuild.Widgetbuild(BuildContext context) {final controller=useAnimationController();final controller2=useAnimationController();returnContainer();}
Hooks are entirely independent of each other and from the widget.
This means that they can easily be extracted into a package and published onpub for others to use.
Similar toState
, hooks are stored in theElement
of aWidget
. However, insteadof having oneState
, theElement
stores aList<Hook>
. Then in order to use aHook
,one must callHook.use
.
The hook returned byuse
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:
classHookElementextendsElement {List<HookState> _hooks;int _hookIndex;Tuse<T>(Hook<T> hook)=> _hooks[_hookIndex++].build(this);@overrideperformRebuild() { _hookIndex=0;super.performRebuild(); }}
For more explanation of how hooks are implemented, here's a great article abouthow it was done in React:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
Due to hooks being obtained from their index, some rules must be respected:
Widgetbuild(BuildContext context) {// starts with `use`, good nameuseMyHook();// doesn't start with `use`, could confuse people into thinking that this isn't a hookmyHook();// ....}
Widgetbuild(BuildContext context) {useMyHook();// ....}
Widgetbuild(BuildContext context) {if (condition) {useMyHook(); }// ....}
Since hooks are obtained from their index, one may think that hot-reloads while refactoring will break the application.
But worry not, aHookWidget
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 ofHookB
after performing a hot-reload:
useA();useB(42);useC();
Here everything works fine and all hooks maintain their state.
Now consider that we removedHookB
. We now have:
useA();useC();
In this situation,HookA
maintains its state butHookC
gets hard reset.This happens because, when a hot-reload is performed after refactoring, all hooksafter the first line impacted are disposed of.So, sinceHookC
was placedafterHookB
, it will be disposed.
There are two ways to create a hook:
A function
Functions are by far the most common way to write hooks. Thanks to hooks beingcomposable by nature, a function will be able to combine other hooks to createa more complex custom hook. By convention, these functions will be prefixed by
use
.The following code defines a custom hook that creates a variable and logs its valueto the console whenever the value changes:
ValueNotifier<T>useLoggedState<T>([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 usingHook.use
.
As a class, the hook will look very similar to aState
class and have access to widgetlife-cycle and methods such asinitHook
,dispose
andsetState
.It is usually good practice to hide the class under a function as such:
ResultuseMyHook() {returnuse(const_TimeAlive());}
The following code defines a hook that prints the total time a
State
has been alive on its dispose.class_TimeAliveextendsHook<void> {const_TimeAlive();@override_TimeAliveStatecreateState()=>_TimeAliveState();}class_TimeAliveStateextendsHookState<void,_TimeAlive> {DateTime start;@overridevoidinitHook() {super.initHook(); start=DateTime.now(); }@overridevoidbuild(BuildContext context) {}@overridevoiddispose() {print(DateTime.now().difference(start));super.dispose(); }}
Flutter_Hooks already comes with a list of reusable hooks which are divided into different kinds:
A set of low-level hooks that interact with the different life-cycles of a widget
Name | Description |
---|---|
useEffect | Useful for side-effects and optionally canceling them. |
useState | Creates a variable and subscribes to it. |
useMemoized | Caches the instance of a complex object. |
useRef | Creates an object that contains a single mutable property. |
useCallback | Caches a function instance. |
useContext | Obtains theBuildContext of the buildingHookWidget . |
useValueChanged | Watches a value and triggers a callback whenever its value changed. |
This category of hooks the manipulation of existing Flutter/Dart objects with hooks.They will take care of creating/updating/disposing an object.
Name | Description |
---|---|
useStream | Subscribes to aStream and returns its current state as anAsyncSnapshot . |
useStreamController | Creates aStreamController which will automatically be disposed. |
useOnStreamChange | Subscribes to aStream , registers handlers, and returns theStreamSubscription . |
useFuture | Subscribes to aFuture and returns its current state as anAsyncSnapshot . |
Name | Description |
---|---|
useSingleTickerProvider | Creates a single usageTickerProvider . |
useAnimationController | Creates anAnimationController which will be automatically disposed. |
useAnimation | Subscribes to anAnimation and returns its value. |
Name | Description |
---|---|
useListenable | Subscribes to aListenable and marks the widget as needing build whenever the listener is called. |
useListenableSelector | Similar touseListenable , but allows filtering UI rebuilds |
useValueNotifier | Creates aValueNotifier which will be automatically disposed. |
useValueListenable | Subscribes to aValueListenable and return its value. |
useOnListenableChange | Adds a given listener callback to aListenable which will be automatically removed. |
A series of hooks with no particular theme.
Name | Description |
---|---|
useReducer | An alternative touseState for more complex states. |
usePrevious | Returns the previous argument called to [usePrevious]. |
useTextEditingController | Creates aTextEditingController . |
useFocusNode | Creates aFocusNode . |
useTabController | Creates and disposes aTabController . |
useScrollController | Creates and disposes aScrollController . |
usePageController | Creates and disposes aPageController . |
useFixedExtentScrollController | Creates and disposes aFixedExtentScrollController . |
useAppLifecycleState | Returns the currentAppLifecycleState and rebuilds the widget on change. |
useOnAppLifecycleStateChange | Listens toAppLifecycleState changes and triggers a callback on change. |
useTransformationController | Creates and disposes aTransformationController . |
useIsMounted | An equivalent toState.mounted for hooks. |
useAutomaticKeepAlive | An equivalent to theAutomaticKeepAlive widget for hooks. |
useOnPlatformBrightnessChange | Listens to platformBrightness changes and triggers a callback on change. |
useSearchController | Creates and disposes aSearchController . |
useWidgetStatesController | Creates and disposes aWidgetStatesController . |
useExpansibleController | Creates aExpansibleController . |
useDebounced | Returns a debounced version of the provided value, triggering widget updates accordingly after a specified timeout duration |
useDraggableScrollableController | Creates aDraggableScrollableController . |
useCarouselController | Creates and disposes aCarouselController . |
useTreeSliverController | Creates aTreeSliverController . |
useOverlayPortalController | Creates and manages anOverlayPortalController for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed. |
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:
Describe the use-case.
Open an issue explaining why we need this hook, how to use it, ...This is important as a hook will not get merged if the hook doesn't appeal toa large number of people.
If your hook is rejected, don't worry! A rejection doesn't mean that it won'tbe merged later in the future if more people show interest in it.In the mean-time, feel free to publish your hook as a package onhttps://pub.dev.
Write tests for your hook
A hook will not be merged unless fully tested to avoid inadvertently breaking itin the future.
Add it to the README and write documentation for it.
About
React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.