Flutter hooks is a powerful library that brings the functionality of React Hooks to Flutter.
Why should we use Hooks?
Simplified Code: Hooks allow us to write more readable code by reducing boilerplate.
Reusable Logic: Hooks enable us to extract and reuse stateful logic across different components.
Lifecycle Management: Hooks help in managing the lifecycle of stateful objects like controllers without needing a StatefulWidget.
In here we are going to ๐ a tutorial that'll help us learn Flutter Hooks.
As always all of the code for the project will be present in the learn_flutter_hooks repository.
Basic Concepts:
In order to use Flutter hooks let's learn the basic concepts
HookWidget: This is a replacement of
StatefulWidget
when using hooks.useState: Manages state within a HookWidget.
useEffect: Executes a function in response to lifecycle events.
useMemoized: Memorizes a value to avoid recomputing it on every build.
useTextEditingController: Manages a TextEditingController.
Basic HookWidget
with useState
Setting up Flutter Hooks.
Add the Flutter Hooks package to yourpubspec.yaml
:
flutter pub add flutter_hooks
Let's create a counter Screen to demonstrate the example of useState
:
class CounterPage extends HookWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
// Use a hook to manage CounterState
final counter = useState(0);
return Scaffold(
appBar: AppBar(
title: const Text('Counter using Hooks'),
),
body: Center(
child: Text(
'Counter: ${counter.value}',
style: const TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// We are adding the value of counter here
counter.value++;
},
child: const Icon(Icons.add),
),
);
}
}
Obviously since we are using the useState()
the page works as intended.
Now in the above tutorial we built two things:
HookWidget:
CounterScreen
extendsHookWidget
instead ofStatefulWidget
.useState: The
useState
hook is used to declare a state variablecounter
and its updatercounter.value
.
You can find the code for the above tutorial here: basic_hooks_concepts
In the next tutorial let's use hooks for a TextEditingController
Tutorial: Build a Login Page using Flutter Hooks
Build the LoginPage using StatefulWidget
First let's build a sign in form using Stateful Widget. Now since we are going to use the TextEditingController
. Let's look at all the fields we'll have to do.
Build a Basic TextField UI to enter an email
Login Page:
First I am creating a Stateful Login Page without widgets as of yet.
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
Add EmailTextField
:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stateful Login Page'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
suffixIcon: IconButton(
onPressed: () {
// We'll implement this later
},
icon: const Icon(Icons.arrow_forward)),
labelText: 'Enter your email',
),
),
),
),
));
}
Add a TextEditingController
to our LoginPage:
Now that we have the email let's add a TextEditingController
named emailController
initialization:
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
dispose:
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
signIn() method:
void signIn() {
if (_formKey.currentState!.validate()) {
final email = _emailController.text;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email: $email'),
),
);
}
}
Validator:
Finally let's add the validator and connect the signIn()
to the suffix Button.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stateful Login Page'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please Enter your email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
suffixIcon: IconButton(
onPressed: signIn, icon: const Icon(Icons.arrow_forward)),
labelText: 'Enter your email',
),
),
),
),
));
}
Perfect. Now when we run the app. We are going to get the following output:
You can find the code for the above Stateful Widget here: stateful_sign_in
Build the LoginPage using FlutterHooks
Before we being there are few things we must learn about hooks:
Hooks can only be used in the build() function of our widgets as we will soon see in the tutorial.
We are going to use a
HookWidget
instead ofStateful
orStateless
Widgets.
So let's create a new page
LoginHooksPage:
Basic Page:
class LoginHooksPage extends HookWidget {
const LoginHooksPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
Now let's initialize the _formKey
and _emailController
:
initialization:
Remember we can use hooks inside the build():
@override
Widget build(BuildContext context) {
final _formKey = useMemoized(() => GlobalKey<FormState>());
final _emailController = useTextEditingController();
return Scaffold();
}
Here's the entire LoginHooksPage:
class LoginHooksPage extends HookWidget {
const LoginHooksPage({super.key});
@override
Widget build(BuildContext context) {
final _formKey = useMemoized(() => GlobalKey<FormState>());
final _emailController = useTextEditingController();
void _signIn() {
if (_formKey.currentState!.validate()) {
final email = _emailController.text;
// Show the email in a SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email: $email'),
),
);
}
}
return Scaffold(
appBar: AppBar(
title: const Text('Hooks Login Page'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
child: TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
suffixIcon: IconButton(
onPressed: _signIn,
icon: const Icon(Icons.arrow_forward),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
},
),
),
),
);
}
}
As we can see it works perfectly:
Perfect, so in the above tutorial. We have used
useMemoized: Used to create and memoize the
GlobalKey<FormState>()
.useTextEditingController: Used to create and manage the
TextEditingController
.
You can find code for the above tutorial in the following branch: hooks_sign_in
Tutorial : Learning useEffect
Now let's dive into useEffect
hook, which is used to perform side effects in a HookWidget
.
What isuseEffect
?
The useEffect hooks allows us to run side-effectful code based on changes to state or props.
It's similar to the lifecycle methods
initState
,didUpdateWidget
,dispose
in a StatefulWidget
Basic Usage:
The useEffect
hook has two parameters:
A function that contains the side-effect code.
A list of dependences that determine when the side-effect function should run.
Example 1: Simple useEffect
Let's start with a simple example where we log a message to the console whenever the component is rendered.
class ExampleScreen extends HookWidget {
const ExampleScreen({super.key});
@override
Widget build(BuildContext context) {
//Using useEffect to log a message on render
useEffect(() {
print('ExampleScreen rendered');
return null; // No cleanup needed
}, []);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Example'),
),
body: const Center(
child: Text('Check the console for messages'),
),
);
}
}
So in the above screen: The effect function runs after the first render and every time the dependencies change.
In the above example :
Since we are tracking an empty list of dependencies
[]
, useEffect() was only called once.But if we are not tracking any dependencies. It would have been called every time.
You can see the example here:
Example 2: useEffect
with Dependencies
Now let's create an example where useEffect
depends on a state variable. we'll update the counter and use useEffect
to log the counter value whenever it changes.
class CounterUEPage extends HookWidget {
const CounterUEPage({super.key});
@override
Widget build(BuildContext context) {
final counter = useState(0);
useEffect(() {
print('Counter Value: ${counter.value}');
return null;
}, [counter.value]);
return Scaffold(
appBar: AppBar(
title: const Text('Counter using useEffect'),
),
body: Center(
child: Text(
'Counter: ${counter.value}',
style: const TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.value++,
child: const Icon(Icons.add),
),
);
}
}
So in the above example we are doing two things:
useState: Manages the
counter
state.useEffect: Runs the effect function whenever the
counter.value
changes. In this case, it logs the new counter value to the console.
Important: What is the point of a dependency aka Key ?
Now let's take a look at the following code:
useEffect(() {
print('Counter Value: ${counter.value}');
return null;
}, [counter.value]);
In here the reason we suggested [counter.value] as a Key is because we don't want useEffect()
to be called every time there is a hot reload. Instead we only want useEffect()
to be called when there is a change in the value of the counter.
Example 3: useEffect
with Cleanup
Sometimes you need to cleanup after a side effect, like unsubscribing from a stream or cleaning a timer.
The useEffect hook can return a cleanup function.
Let's create a new file named TimerUEPage
to show useEffect cleanup with a timer
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class TimerUEPage extends HookWidget {
const TimerUEPage({super.key});
@override
Widget build(BuildContext context) {
final time = useState(0);
// Using useEffect to start a timer and clean it up when the component is unmounted
useEffect(() {
final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
time.value++;
});
return () => timer.cancel(); // Cleanup function to cancel the timer
});
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Example with Cleanup'),
),
body: Center(
child: Text(
'Time: ${time.value} seconds',
style: const TextStyle(fontSize: 24),
),
),
);
}
}
These are the two main concepts of the above page:
useState: Manages the
time
state.useEffect: Starts a timer that increments the
time
value every second. The cleanup function cancels the timer when the component is unmounted.
As you can see it works:
Example 4: Applying useEffect
in SignIn Page
Now let's apply useEffect
to the sign-in form example to demonstrate a side effect, such as logging the email value when it changes.
Let's create a page named LoginUEPage
class LoginUEPage extends StatelessWidget {
const LoginUEPage({super.key});
@override
Widget build(BuildContext context) {
final _formKey = useMemoized(() => GlobalKey<FormState>());
final _emailController = useTextEditingController();
useEffect(() {
print('Email: ${_emailController.text}');
return null; // No Cleanup needed
}, [_emailController.text]);
void _signIn(){
................
}
}
Now in this scenario:
- useEffect: Logs the email value whenever it changes by including _emailController.text
in the dependencies list.
This covers the basics of useEffect
. You can use it to handle various side effects in your application, making your code cleaner and more manageable.
You can find the code for useEffect in here: hooks_useEffect
Source Code:
You can find the total source code of the app here: learn_flutter_hooks