flutter_hooks는 Dart에 React 스타일의 훅을 가져오는 Flutter 패키지로, 개발자가 위젯 내에서 재사용 가능한 상태 로직과 부작용을 만들 수 있게 해줍니다. 상태와 동작을 사용자 정의 훅으로 추출할 수 있어 코드 구조를 단순화하고, 반복 코드를 줄이며 가독성을 높입니다. 이 패키지는 모바일, 웹, 데스크톱을 포함한 모든 주요 플랫폼을 지원하여 컴포넌트 간 공유 로직이 있는 유지보수 용이한 UI를 구축하는 데 이상적입니다.
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:
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:
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 result
may vary between compilation fails to unknown behavior.
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:
They can only be used in the build method of a widget that mix-in Hooks.
The same hook can be reused arbitrarily many times.
The following code defines two independent AnimationController, and they are
correctly 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 on
pub for others to use.
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:
classHookElementextendsElement{
List<HookState> _hooks;
int _hookIndex;
T use<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 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:
Widgetbuild(BuildContext context) {
// starts with `use`, good nameuseMyHook();
// doesn't start with `use`, could confuse people into thinking that this isn't a hookmyHook();
// ....
}
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 afterHookB, it will be disposed.
How to create a hook
There are two ways to create a hook:
A function
Functions are by far the most common way to write hooks. Thanks to hooks being
composable by nature, a function will be able to combine other hooks to create
a 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 value
to 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 using Hook.use.
As a class, the hook will look very similar to a State class and have access to widget
life-cycle and methods such as initHook, dispose and setState.
It is usually good practice to hide the class under a function as such:
Creates and manages an OverlayPortalController for controlling the visibility of overlay content. The controller will be automatically disposed when no longer needed.
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 to
a large number of people.
If your hook is rejected, don't worry! A rejection doesn't mean that it won't
be 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 on https://pub.dev.
Write tests for your hook
A hook will not be merged unless fully tested to avoid inadvertently breaking it
in the future.
Add it to the README and write documentation for it.