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 returnsValueNotifier<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,
),
),
),
);
}
}
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:
useStream – subscribe to events
Stream
useFuture – subscribe to events
Future
useStreamController – we create
StreamController
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.