Flutter Hooks, everything to know about them

Flutter Hooks, everything to know about them

ยท

8 min read

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

  1. HookWidget: This is a replacement of StatefulWidget when using hooks.

  2. useState: Manages state within a HookWidget.

  3. useEffect: Executes a function in response to lifecycle events.

  4. useMemoized: Memorizes a value to avoid recomputing it on every build.

  5. 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.

useState in Hooks

Now in the above tutorial we built two things:

  1. HookWidget: CounterScreen extends HookWidget instead of StatefulWidget.

  2. useState: The useState hook is used to declare a state variable counter and its updater counter.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:

Using Stateful Widget

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 of Stateful or Stateless 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:

hooks email

Perfect, so in the above tutorial. We have used

  1. useMemoized: Used to create and memoize the GlobalKey<FormState>().

  2. 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:

  1. A function that contains the side-effect code.

  2. 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 :

  1. Since we are tracking an empty list of dependencies [], useEffect() was only called once.

  2. 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:

  1. useState: Manages the counter state.

  2. 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:

  1. useState: Manages the time state.

  2. 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:

Cleanup using useEffect

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

Did you find this article valuable?

Support Harish Kunchala by becoming a sponsor. Any amount is appreciated!

ย