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
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!
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);
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.
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
});
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
}
});
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]);
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]);
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;
}
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
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(() {});
}
}
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 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.
Top comments (0)