Why use Hooks
During the development of the Flutter project, it was easy to find that the high coupling of business logic and view logic was a pain point that front-end development often encountered. To solve this problem, a Mixin approach can be used in Flutter, but there are some limitations in practice:
- Mixins may depend on each other.
- There may be conflicts between mixins.
Hooks solve these limitations. Use Hooks to find out.
How to use Hooks
To introduce Hooks, add the following to pubspec.yaml
Dependencies: flutter_hooks: ^ 0.14.1Copy the code
Run the download package command
$ flutter pub get
Copy the code
Introduced in the used Dart package
import 'package:flutter_hooks/flutter_hooks.dart';
Copy the code
Use flutter_hooks. Remember that the names of flutter_hooks begin with use, useState,useEffect, etc.
-
useState
Just like the project Flutter has just created, click the button and increment the number. Here is a code to see how this is done using Hooks:
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(MaterialApp( home: HooksExample(), )); } class HooksExample extends HookWidget { @override Widget build(BuildContext context) { final counter = useState(0); return Scaffold( appBar: AppBar( title: const Text('useState example'), ), body: Center( child: Text('Button tapped ${counter.value} times'), ), floatingActionButton: FloatingActionButton( onPressed:() => counter.value++, child: const Icon(Icons.add), ), ); }}Copy the code
On the surface, a HooksExample inherits a HookWidget. In fact, a HookWidget inherits a StatelessWidget, so a HooksExample is a StatelessWidget. So why did it change state? Isn’t it only statefulWidgets that can change state? UseState (0) = 0; counter. Value++ = counter. Value++ = counter. How easy is it to separate the business logic from the view logic? Can you access and maintain state without using the StatefulWidget? Keep your questions open, and we’ll uncover the secrets behind these two methods later.
Of course, we can also import multiple Hooks in the same Widget:
final counter = useState(0); Final name = useState(' zhang '); final counter2 = useState(100);Copy the code
Do not use conditional statements like the following when using Hooks:
if(condition) {
useMyHook();
}
Copy the code
-
useEffect
This method is used when you need to initialize an object and do some cleaning at the end of the Widget’s life cycle. Look at the following code and you’ll see:
class MyWidget extends HookWidget { @override Widget build(BuildContext context) { final store = useMemoized(() => MyStore()); useEffect((){ store.init(); return store.dispose; },const []); return Scaffold(...); } } Copy the code
UseEffect does some initialization inside the input function. If you need to do some cleanup at the end of the Widget’s life cycle, you can return a cleanup function, such as store.Dispose in your code. The second input parameter to useEffect is an empty array, ensuring that the initialization and cleanup functions are called only once at the beginning and end of the Widget’s life cycle. If this parameter is not passed, crash will occur.
In addition to these Hooks, Flutter_hooks also provide Hooks that can save us a lot of code. Hanged to useMemoized, useValueChanged, useAnimation, useFuture, etcflutter_hooks@githubLook at it.
-
Customize the hook
There are also two ways to customize hooks when the hooks provided by Flutter_hooks do not meet our requirements.
1, A function
So far, function is the most commonly used method to customize hooks, because hooks can be combined, so you can combine hooks in custom functions to create custom hooks. Of course, custom hooks should also start with use.
Here is a custom hook. Pass in a variable and print it out if it changes.
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) { final result = useState<T>(initialData); useValueChanged(result.value, (_, __) { print(result.value); }); return result; } Copy the code
UseLoggedState obviously combines useState and useValueChanged to complete useLoggedState.
2, A class
When a hook is too complex, you can create a custom class that inherits the hook so that it looks like a State and has access to life-cycle methods such as initHook, Dispose, and setState. It’s a good idea to hide a class in a function like this:
Result useMyHook(BuildContext context) { return use(const _TimeAlive()); } Copy the code
The following is a custom hook that prints the lifetime of a state:
class _TimeAlive extends Hook<void> { const _TimeAlive(); @override _TimeAliveState createState() => _TimeAliveState(); } class _TimeAliveState extends HookState<void, _TimeAlive> { DateTime start; @override void initHook() { super.initHook(); start = DateTime.now(); } @override void build(BuildContext context) {} @override void dispose() { print(DateTime.now().difference(start)); super.dispose(); }}Copy the code
Explore the underlying implementation of Hooks
With that in mind, let’s look at the underlying implementation. Start with useState!
ValueNotifier<T> useState<T>([T initialData]) {
return use(_StateHook(initialData: initialData));
}
R use<R>(Hook<R> hook) => Hook.use(hook);
Copy the code
In the useState method, the use method is called and the _StateHook object is created as an argument. If you look at the useState method, you can see that _StateHook and _StateHookState are the same. If you look more closely, it looks like a custom StatefullWidget. The Hook inherited from _StateHook and the HookState inherited from _StateHookState are widgets and states. Well, that’s easy to understand. Moving on… Here is the code for all four classes (only some of the key code is shown) :
Hook:
abstract class Hook<R> {
const Hook({this.keys});
static R use<R>(Hook<R> hook) {
return HookElement._currentHookElement._use(hook);
}
@protected
HookState<R, Hook<R>> createState();
}
Copy the code
_StateHook class:
class _StateHook<T> extends Hook<ValueNotifier<T>> {
const _StateHook({this.initialData});
final T initialData;
@override
_StateHookState<T> createState() => _StateHookState();
}
Copy the code
HookState class:
abstract class HookState<R, T extends Hook<R>> { @protected BuildContext get context => _element; HookElement _element; T get hook => _hook; T _hook; void deactivate() {} @protected void setState(VoidCallback fn) { fn(); _element .. _isOptionalRebuild = false .. markNeedsBuild(); }}Copy the code
_StateHookState class:
class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
ValueNotifier<T> _state;
@override
void initHook() {
super.initHook();
_state = ValueNotifier(hook.initialData)..addListener(_listener);
}
@override
void dispose() {
_state.dispose();
}
@override
ValueNotifier<T> build(BuildContext context) {
return _state;
}
void _listener() {
setState(() {});
}
}
Copy the code
InitialData is stored in the _StateHook object, which calls the use method of the Hook class. As you can see above, it calls the _use method of the HookElement class. Let’s look at what a HookElement is.
mixin HookElement on ComponentElement { static HookElement _currentHookElement; _Entry<HookState> _currentHookState; final LinkedList<_Entry<HookState>> _hooks = LinkedList(); LinkedList<_Entry<bool Function()>> _shouldRebuildQueue; LinkedList<_Entry<HookState>> _needDispose; bool _isOptionalRebuild = false; Widget _buildCache; @override Widget build() { // Check whether we can cancel the rebuild (caused by HookState.mayNeedRebuild). final mustRebuild = _isOptionalRebuild ! = true || _shouldRebuildQueue.any((cb) => cb.value()); _isOptionalRebuild = null; _shouldRebuildQueue? .clear(); if (! mustRebuild) { return _buildCache; } if (kDebugMode) { _debugIsInitHook = false; } _currentHookState = _hooks.isEmpty ? null : _hooks.first; HookElement._currentHookElement = this; try { _buildCache = super.build(); } finally { _unmountAllRemainingHooks(); HookElement._currentHookElement = null; if (_needDispose ! = null && _needDispose.isNotEmpty) { for (var toDispose = _needDispose.last; toDispose ! = null; toDispose = toDispose.previous) { toDispose.value.dispose(); } _needDispose = null; } } return _buildCache; } R _use<R>(Hook<R> hook) { /// At the end of the hooks list if (_currentHookState == null) { _appendHook(hook); } else if (hook.runtimeType ! = _currentHookState.value.hook.runtimeType) { final previousHookType = _currentHookState.value.hook.runtimeType; _unmountAllRemainingHooks(); if (kDebugMode && _debugDidReassemble) { _appendHook(hook); } else { } } else if (hook ! = _currentHookState.value.hook) { final previousHook = _currentHookState.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { _currentHookState.value .. _hook = hook .. didUpdateHook(previousHook); } else { _needDispose ?? = LinkedList(); _needDispose.add(_Entry(_currentHookState.value)); _currentHookState.value = _createHookState<R>(hook); } } final result = _currentHookState.value.build(this) as R; _currentHookState = _currentHookState.next; return result; }}Copy the code
In the _use method, we can see two key points:
1. _appendHook(hook); Go to the _appendHook method and find the class extension of HookElement, which calls the method that creates HookState and calls the initHook() method of HookState, The listeners on initHook() are wrapped and assigned to _listeners and added to _listeners on _listeners. The listeners on initHook() are setState(() {}). Most importantly, it assigns the current HookElement object to the _Element property of HookState. What is the current HookElement? Look down with this question.
2, the other is a _currentHookState. Value. Build (this) as R; _currentHookState is an _Entry variable, so it calls the build method of the HookState subclass, _StateHookState. You can see here that the final return value of the external call to useState is generated here.
extension on HookElement { HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) { assert(() { _debugIsInitHook = true; return true; } (), "); final state = hook.createState() .. _element = this .. _hook = hook .. initHook(); assert(() { _debugIsInitHook = false; return true; } (), "); return state; } void _appendHook<R>(Hook<R> hook) { final result = _createHookState<R>(hook); _currentHookState = _Entry(result); _hooks.add(_currentHookState); }}Copy the code
ValueNotifier is a set method that calls notifyListeners() whenever a value changes.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
ValueNotifier(this._value);
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
Copy the code
ValueNotifier parent method:
void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners ! = null) { final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners); for (final VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: ErrorDescription('while dispatching notifications for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty<ChangeNotifier>( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, ); })); }}}}Copy the code
The notifyListeners call the previously saved setState(() {}) method, followed by the markNeedsBuild() method of the HookState saved _Element, Finally, call HookElement’s Build method to rerender the interface. All of a sudden, there’s a HookElement here, the same one we were wondering about, but when was the HookElement created? Guess, is it the Element of a HookWidget? Let’s take a look at what the HookWidget class does.
abstract class HookWidget extends StatelessWidget {
/// Initializes [key] for subclasses.
const HookWidget({Key key}) : super(key: key);
@override
_StatelessHookElement createElement() => _StatelessHookElement(this);
}
class _StatelessHookElement extends StatelessElement with HookElement {
_StatelessHookElement(HookWidget hooks) : super(hooks);
}
Copy the code
The HookWidget class is simple. When the HookWidget is constructed, the createElement method is implicitly called. The _StatelessHookElement object is generated, and the mount method of StatelessElement is called. HookElement._currentHookElement = this; HookElement. Finally call widget.build(this) in StatelessElement; There is no detail on the underlying rendering logic here, see the rendering principles of Widgets in my other article.
To summarize
Now that we’ve talked about it, we can sort out the whole logic, Through inheritance HookWidget created a StatelessWidget and StatelessElement StatelessElement through mix A HookElement can manage multiple states. Each use creates a state for the HookElement to manage. When the value changes, the setState method of the state is called. Make the HookWidget’s HookElement call the HookWidget’s Build method to rerender. Flutter_hooks hoook Element.
While using Hooks can greatly simplify our development, it is important to note that Flutter_hooks do not handle passing state between widgets, so use Hooks in conjunction with state management tools like Provider.
Finally, I hope that reading this article will give you some insight into the use of Hooks techniques in Flutter. If there are any mistakes in the article, or we have any ideas, please mention in the comments, discuss with each other, thank you very much.