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.