Applying Hooks from React to Flutter

Recently I came across a hook implementation for Flutter, which I want to talk about.

Why use hooks in Flutter?

The reasons are the same as why people use them in React, namely:

In particular, an unpleasant moment with Statefull widgets – a large number of boiler plates with initState and dispose… The authors of the implementation describe the problem with the following code:

class Example extends StatefulWidget {
  final Duration duration;

  const Example({Key key, required this.duration})
      : 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();
  }
}

I think by looking at the code, you understand the pain that its authors experienced as well.

Not wanting to put up with the pain, they remembered that there is such a nice thing as hooks, and how convenient it is to use them.

And this is how the same example looks like, but using hooks:

class Example extends HookWidget {
  const Example({Key key, required this.duration})
      : super(key: key);

  final Duration duration;

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

Small technical educational program

Hooks are fine, but there is a requirement – you cannot use conditions when using hooks (see example).

@override
Widget build(BuildContext context) {
  if (isCondition) {
    final val = useHook();
    /// ... some code
  } else {
   	final val2 = useHook2();
    /// ... some code
  }
}

If you are interested in how hooks work and why this is not magic, then the authors recommend reading about the internal structure of hooks. here

In short, the order in which the hooks are called is important. Inside each widget there is a counter, using a hook, the counter is incremented, and due to this, it is possible to distinguish where which hook is (in fact, because of this, the restriction on ifs appeared).

How to use?

First you need to add the library to your flutter project. flutter_hooks

flutter pub add flutter_hooks

Now let’s take a closer look at what capabilities this library provides.

Let’s say we have a starter application that Flutter generates. Here is a classic counter of button clicks.

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Now let’s rewrite it to hooks. For this we will use useState and useCallback

  • useState – creates a state and returns ValueNotifier<T>

    Whenever the ValueNotifier is updated, the widget ( ValueListenableBuilder use optional). At the first call, you need to specify the initial value, then it is ignored.

  • useCallback – function caching based on a list of keys.

    Convenient useMemoized wrapper for callbacks

  • useMemoized – caching a value based on a list of keys.

class MyHomePage extends HookWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    final counter = useState(0);
    final increment = useCallback(() => counter.value++, [counter]);
    // or useMemoized(() => () => counter.value++, [counter]);
    
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counter.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Okay, let’s do something more interesting.

Let’s say we want to use Stream… There is a hook for this useStream… First, let’s write a simple Stream with flowers.

Stream<Color> colorsStream() async* {
  final myColors = [
    Colors.blue,
    Colors.red,
    Colors.yellow,
    Colors.grey,
    Colors.green,
  ];
  while (true) {
    for (final color in myColors) {
      await Future.delayed(const Duration(milliseconds: 1000));
      yield color;
    }
  }
}

Stream is, let’s use.

class MyHomePage extends HookWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final stream = useMemoized(() => colorsStream());
    final color = useStream(stream, initialData: Colors.white);

    return Scaffold(
      body: Center(
        child: AnimatedContainer(
          height: 300,
          width: 300,
          duration: const Duration(milliseconds: 500),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            color: color.data,
          ),
        ),
      ),
    );
  }
}
This is what the result looks like
This is what the result looks like

Using useStream call build will occur for each new event, so consider when you create Stream in method build

The functionality of the library does not end there, it provides a large number of methods:

Description of methods and links to documentation

Basic primitives:

Hooks for working with asynchronous operations:

Hooks for managing the state:

Hooks for working with animations:

Rest:

Outcome

That’s all, thanks for your time, I hope you are as interested in the idea of ​​using hooks in Flutter as I am.

Similar Posts

Leave a Reply