DEV Community

Cover image for How to use Flutter Hooks
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to use Flutter Hooks

Written by Chidume Nnamdi ✏️

Hooks, meet Flutter. Inspired by React Hooks and Dan Abramov's piece, Making sense of React Hooks, the developers at Dash Overflow decided to bring Hooks into Flutter.

Flutter widgets behave similarly to React components, as many of the lifecycles in a React component are present in a Flutter widget. According to the creators on their GitHub page:

Hooks are a new kind of object that manages Widget life-cycles. They exist for one reason: increase the code-sharing between widgets by removing duplicates.

The flutter_hooks library provides a robust and clean way to manage a widget's lifecycle by increasing code-sharing between widgets and reducing duplicates in code.

The built-in Flutter Hooks include:

  • useEffect
  • useState
  • useMemoized
  • useRef
  • useCallback
  • useContext
  • useValueChanged

In this post, we’ll focus on three of these Hooks:

  • The useState Hook manages local states in apps
  • The useEffect Hook fetches data from a server and sets the fetch to the local state
  • The useMemoized Hook memoizes heavy functions to achieve optimal performance in an app

We’ll also learn how to create and use custom Hooks from flutter_hooks as well.

Now, let's see how we can install the flutter_hooks library below.

Installing the flutter_hooks library

To use Flutter Hooks from the flutter_hooks library, we must install it by running the following command in a terminal inside a Flutter project:

flutter pub add flutter_hooks
Enter fullscreen mode Exit fullscreen mode

This adds flutter_hooks: VERSION_NUMER_HERE in the pubspec.yaml file in the dependencies section.

Also, we can add flutter_hooks into the dependencies section in the pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks:
Enter fullscreen mode Exit fullscreen mode

After saving the file, Flutter installs the dependency. Next, import the flutter_hooks library:

import 'package:flutter_hooks/flutter_hooks.dart';
Enter fullscreen mode Exit fullscreen mode

Now we are good to go!

The useState Hook

Just like useState in React, useState in Flutter helps us create and manage state in a widget.

The useState Hook is called with the state we want to manage locally in a widget. This state passes to the useState Hook as a parameter. This state is the initial state because it can change during the lifetime of the widget:

final state = useState(0);
Enter fullscreen mode Exit fullscreen mode

Here, 0 passes to useState and becomes the initial state.

Now, let's see how we can use it in a widget. We must first convert Flutter's counter example to use useState.

Here is Flutter's original counter example:

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

  final String title;

  @override
  _MyHomePageState 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>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that using the StatefulWidget makes maintaining state locally in a widget complex at times. We must also introduce another class that extends a State class, creating two classes for a StatefulWidget.

However, with Hooks, we only use one class to maintain our code, making it easier to maintain than StatefulWidget.

Below is the Hook equivalent:

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

  final String title;

  @override
  Widget build(BuildContext context) {
    final _counter = useState(0);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter.value',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _counter.value++,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The Hook example is shorter than its contemporary. Before using Flutter Hooks in a widget, however, the widget must extend HookWidget, which is provided by the flutter_hooks library.

By calling useState in the build method with 0, we store the returned value in _counter. This _counter is an instance of ValueNotifier.

The state is now stored at the .value property of the ValueNotifier. So, the value of the _counter state is stored at _counter.value.

useState subscribes to the state in the .value property and when the value at .value is modified, the useState Hook rebuilds the widget to display the new value.

In the FloatingActionButton, the _counter.value increments if the button is pressed. This makes the state increase by 1, and useState rebuilds the MyHomePage widget to display the new value.

The useEffect Hook

The useEffect Hook in Flutter is the same as React's useEffect Hook. The Hook takes a function callback as a parameter and runs side effects in a widget:

useEffect( () {
    // side effects code here.
    //subscription to a stream, opening a WebSocket connection, or performing HTTP requests
});
Enter fullscreen mode Exit fullscreen mode

Side effects can include a stream subscription, opening a WebSocket connection, or performing HTTP requests. They’re also done inside the Hook, so we can cancel them when a widget is disposed of.

The function callback must return a function and is called when the widget is disposed of. We can then cancel subscriptions or other cleanups in that function before the widget is removed from the UI and widget tree. Other cleanups include:

  • Unsubscribing from a stream
  • Canceling polling
  • Clearing timeouts
  • Canceling active HTTP connections
  • Canceling WebSockets connections

This prevents open connections — such as HTTP, WebSocket connections, open streams, and open subscriptions — in the widget from sticking around after the widget that opened them is destroyed and no longer in the widget tree:

useEffect( () {
    // side effects code here.
    // - Unsubscribing from a stream.
    // - Cancelling polling
    // - Clearing timeouts
    // - Cancelling active HTTP connections.
    // - Cancelling WebSockets conncetions.
        return () {
        // clean up code
    }
});
Enter fullscreen mode Exit fullscreen mode

The function callback in useEffect is called synchronously, meaning it’s called every time the widget renders or rerenders.

keys argument for useEffect

This Hook also has an optional second argument named keys. The keys argument is a list of values that determine whether the function callback in the useEffect Hook will be called or not.

useEffect compares the current values of keys against its previous values. If the values are different, useEffect runs the function callback. If only one value in keys remains the same, the function callback is not called:

useEffect( () {
    // side effects code here.
    return () {
        // clean up code
    }
}, [keys]);
Enter fullscreen mode Exit fullscreen mode

The useMemoized Hook

The useMemoized Hook is like useMemo in React: it memoizes/caches the instance of complex objects created from a builder function.

This function passes to the useMemoized Hook, then useMemoized calls and stores the result of the function. If a widget rerendering the function is not called, useMemoized is called and its previous result returns.

keys argument for useMemoized

Similar to useEffect, the useMemoized Hook has a second optional argument called the keys:

const result = useMemoized(() {}, [keys]);
Enter fullscreen mode Exit fullscreen mode

This keys argument is a list of dependencies, which determine whether the function passed to useMemoized executes when the widget rerenders.

When a widget rebuilds, useMemoized checks its keys to see whether the previous values changed. If at least one value changed, the function callback in the useMemoized Hook will be called, and the result renders the function call result.

If none of the values changed since they were last checked, useMemoized skips calling the function and uses its last value.

Custom Hooks

flutter_hooks enables us to create our own custom Hooks through two methods: a function or class.

When creating custom Hooks, there are two rules to follow:

  • Using use as a prefix tells developers that the function is a Hook, not a normal function
  • Do not render Hooks conditionally, only render the Hook’s result conditionally

Using the function and class methods, we will create a custom Hook that prints a value with its debug value, just like React’s useDebugValue Hook.

Let’s begin with the function method.

Function method

To begin with the function method, we must create a method using any of the built-in Hooks inside it:

ValueNotifier<T> useDebugValue([T initialState],debugLabel) {
    final state = useState(initialState);
    print(debugLabel + ": " + initialState);
    return state;
}
Enter fullscreen mode Exit fullscreen mode

In the above code, using the built-in useState Hook holds the state in the function and prints the state’s debugLabel and value.

We can then return the state. So, using debugLabel, the state’s label prints in the console when the widget is mounted to the widget tree for the first time and when modifying the state value.

Next, let’s see how to use the useDebugValue Hook we created to print the debutLabel string and corresponding state when mounting and rebuilding the widget:

final counter = useDebugValue(0, "Counter");
final score = useDebugValue(10, "Score");

// Counter: 0
// Score: 10
Enter fullscreen mode Exit fullscreen mode

Class method

Now, let's use a class to recreate the useDebugValue custom Hook. This is done by creating a class that extends a Hook class:

ValueNotifier<T> useDebugValue<T>(T initialData, debugLabel) {
  return use(_StateHook(initialData: initialData, debugLabel));
}

class _StateHook<T> extends Hook<ValueNotifier<T>> {
  const _StateHook({required this.initialData, this.debugLabel});

  final T debugLabel;
  final T initialData;

  @override
  _StateHookState<T> createState() => _StateHookState();
}

class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
  late final _state = ValueNotifier<T>(hook.initialData)
    ..addListener(_listener);

  @override
  void dispose() {
    _state.dispose();
  }

  @override
  ValueNotifier<T> build(BuildContext context) {
    print(this.debugLabel + ": " + _state.value);
      return _state;
  }

  void _listener() {
    setState(() {});
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we have the useDebugValue function, which is our custom Hook. It accepts arguments, such as the initialData initial state value the Hook manages, and the state’s label, debugLabel.

The _StateHook class is where our Hook logic is written. When the use function is called and passed in the _StateHook class instance, it registers the _StateHook class to the Flutter runtime. We can then call useDebugLabel as a Hook.

So, whenever creating a Hook using the class method, the class must extend a Hook class. You can also use Hook.use() in place of use().

Conclusion

flutter_hooks brought a major change in how we build Flutter widgets by helping reduce the size of a codebase to a considerably smaller size.

As we have seen, flutter_hooks enables developers to do away with widgets like StatefulWidget, allowing them to write clean and maintainable code that’s easy to share and test.


LogRocket: Full visibility into your web apps

LogRocket Dashboard Free Trial Banner

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Try it for free.

Oldest comments (0)