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:
useState
Hook manages local states in appsuseEffect
Hook fetches data from a server and sets the fetch to the local stateuseMemoized
Hook memoizes heavy functions to achieve optimal performance in an appWe’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.
flutter_hooks
libraryTo 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
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:
After saving the file, Flutter installs the dependency. Next, import the flutter_hooks
library:
import 'package:flutter_hooks/flutter_hooks.dart';
Now we are good to go!
useState
HookJust 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);
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. ); } }
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. ); } }
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.
useEffect
HookThe 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 });
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:
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 } });
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]);
useMemoized
HookThe 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]);
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.
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:
use
as a prefix tells developers that the function is a Hook, not a normal functionUsing 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.
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; }
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
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(() {}); } }
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()
.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "How to use Flutter Hooks"
Custom hooks are used to combine many primary hooks. There is no point in creating custom hook with a single primary hook. Can you add one more example for a custom hook with two primary hooks?
‘$_counter.value’ doesn’t work! it should be ‘${_counter.value}