As a Flutter Developer, you may need to be made aware of the concept of Hooks. This was first introduced in React programming, having the ability to increase code sharing between the widgets and remove duplicates. However, this concept was later acquired by Flutter itself. In this blog post, we shall highlight some stuff about this feature. So let’s explore. Here’s why you should use Flutter Hooks – An interesting concept.
Table of Contents
– Why Hooks?
We have got the basic definition of what hooks are. Let’s understand this with an example.
When we create a new Flutter project, we get to see this source code:
- The default Counter App
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
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),
),
);
}
}
If we observe the cycle behind this code snippet, we shall see that the states are becoming repeated.
That’s when Flutter Hooks comes to the rescue.
Hooks provide the following:
- Increases code sharing between widgets
- Reduces the boilerplate code
- Removes the snippet under Stateful Widget (return state method)
Note: If you have a deep understanding of Flutter, you might say that mixins could be a great replacement for Hooks. Moreover, it also reduces the boilerplate code too.
But it has its limitations:
- It can be used only once.
- mixins and class share the same object
Note: Since React and Flutter are both declarative programming languages, Hooks best fits in Flutter’s Stateful Widget.
So let’s dive deep down and understand this concept via some examples but before that,
Installing the dependency
Under the project’s terminal, add this command:
flutter pub add flutter_hooks
Or install it manually: https://pub.dev/packages/flutter_hooks
Here’s a sample code:
AnimationController
class Example extends StatefulWidget {
final Duration duration;
final Widget child;
const Example({Key? key, required this.duration, required this.child})
: 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 widget.child;
}
}
The above snippet can animate the Widget that has been passed as a child parameter.
The Hook’s proposed solution:
class Example extends HookWidget {
const Example({Key? key, required this.duration, required this.child})
: super(key: key);
final Duration duration;
final Widget child;
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return child;
}
}
You see the difference in how much the code is reduced. That’s the power of Hooks.
Besides the definition, Flutter Hooks also come up with pre-built hooks/components, also known as primitives, which interact with the Widget’s life-cycle states. The above-mentioned useAnimationController is one of them.
Furthermore, here’s the complete list with short descriptions available:
useEffect
Useful for fetching data, streams, canceling subscriptions, etc
useState
useState in React acts as setState in Flutter inside a BuildContext
useMemoized
Keeps a complex object cache inside the memory
useRef
Creates an object having a single mutable property
useCallback
Keeps an instance of functions inside memory
useContext
Obtains BuildContext for HookWidgets (replaces StatefulWidget)
useValueChanged
Watches a value and triggers it when there is a change
Additionally, there are a lot of them but not relevant to the examples that we have for this blog post.
You can find them here.
https://pub.dev/packages/flutter_hooks#existing-hooks
We’re done with the first section. Let’s see what we have for the project structure.
– Project Structure
There will be two examples, namely:
- Flutter’s default Counter App
- HTTP Request handling with Flutter Hooks
Flutter’s default Counter App
Remember the first code snippet you saw?
We shall be taking that code into account and applying Hook’s approach to it.
Replace createState
method
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
}
with
class MyHomePage extends HookWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
}
Moreover, copy the code containing the buildContext method
.
//Extended Clas
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
}
//In Hooks, no need for this class, as we paste buildContext inside Hook Widget (just as we do with Stateless Widget)
By doing this, our final output is ready.
class MyHomePage extends HookWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
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',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
You see, there is a clear difference between the two code snippets, how much the code is reduced. Furthermore, you’ll see errors about _counter and callback events. (ignore that for now).
Let’s implement the functionality approach via Hooks
Initially, declare _counter
variable with ValueNotifier
ValueNotifier<int> _counter = useState(0);
Why ValueNotifier?
As you go deep inside setState, you get to see that it uses the ValueNotifier technique to cope when a new value is triggered.
Additionally, now our _counter
variable is available as:
_counter.value
Moreover, for the callback, just use
_counter.value++
Here’s the working code snippet
class MyHomePage extends HookWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
ValueNotifier<int> _counter = useState(0);
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.toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _counter.value++,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
UI View
No change to the output, and better clarity.
This example was easy. Isn’t it?
Now, let’s move on to another example in the next section.
HTTP Request handling with Flutter Hooks
Fetching a list of To-dos.
Note: I have already created the model class.
Here’s the code snippet that contains the parsed JSON API
class _MyAppState extends State<MyApp> {
List<TodoModel> modelList = [];
Future<List<TodoModel>> allTodos() async {
var response = await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos/"));
if(response.statusCode == 200) {
List<TodoModel> data = todoModelFromJson(response.body);
modelList = data;
return modelList;
}
else {
return [];
}
}
@override
initState() {
super.initState();
allTodos();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView(
shrinkWrap: true,
children: modelList.map((e) {
return ListTile(
title: Text(e.title!),
subtitle: Text(e.completed.toString()),
);
}).toList(),
),
),
);
}
}
In this snippet, with the use of InitState, the data gets loaded to our defined List and fills the UI.
Let’s convert this approach to Hooks.
Code snippet
class MyApp extends HookWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView(
shrinkWrap: true,
children: modelList.value.map((e) {
return ListTile(
title: Text(e.title!),
subtitle: Text(e.completed.toString()),
);
}).toList(),
),
),
);
}
}
Don’t worry about errors, I just converted the Stateful to Stateless Widget and replaced it with HookWidget. The same we did for the first example as well.
Now, paste the snippets of the async function and the defined List.
ValueNotifier<List<TodoModel>> modelList = useState([]);
Future<List<TodoModel>> allTodos() async {
var response = await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos/"));
if(response.statusCode == 200) {
List<TodoModel> data = todoModelFromJson(response.body);
modelList.value = data;
print("data $modelList");
return modelList.value;
}
else {
return [];
}
}
In addition to that, wherever you find modelList
, just replace it with modelList.value
.
- So, up till now, I believed this wouldn’t have been hectic for you guys.
A question may arise: how and where to initialize this function?
Well, we need to make use of a Hook’s reusable component i.e. useEffect
, as it says:
useful for fetching data, streams, canceling subscriptions, etc
Let’s first look at the basic structure of useEffect
:
useEffect(
() //base condition
{}, //async functions "return statement"
[], // Keys
)
For this scenario, the [] is the key that’s been compared by useEffect
itself. Moreover, if the keys are different, then the async callback gets called repeatedly, causing memory leaks, but if only a single value in the key remains the same, then the async callback doesn’t get called.
We shall be passing the empty-list
to the keys section.
Paste this code inside the buildContext
method
useEffect( () {
var result = allTodos();
return () {
result;
};
}, []);
Here’s the updated code snippet
class MyApp extends HookWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
ValueNotifier<List<TodoModel>> modelList = useState([]);
Future<List<TodoModel>> allTodos() async {
var response = await http.get(Uri.parse("https://jsonplaceholder.typicode.com/todos/"));
if(response.statusCode == 200) {
List<TodoModel> data = todoModelFromJson(response.body);
modelList.value = data;
print("data $modelList");
return modelList.value;
}
else {
return [];
}
}
useEffect( () {
var result = allTodos();
return () {
result;
};
}, []);
return MaterialApp(
home: Scaffold(
body: ListView(
shrinkWrap: true,
children: modelList.value.map((e) {
return ListTile(
title: Text(e.title!),
subtitle: Text(e.completed.toString()),
);
}).toList(),
),
),
);
}
}
UI View
We get this output with no memory leaks at all.
That’s all! Thank you so much guys for your sincere attention.
Wrapping up
Hope you enjoyed every bit of it.
In this blog post: Here’s why you should use Flutter Hooks – Interesting concept. I tried my best, up to my limited knowledge, to make this fit for a beginner to a pro-level developer.
However, if you think something has been missed or want to add your point of view, please jot it down in the comments section. I will be more than happy to respond.
Furthermore, you can connect with me on:
Link to GitHub Repositories, and YouTube Channel
Read out previous blogs – Click here