Here’s why you should use Flutter Hooks – Interesting concept

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

Counter app with Hooks

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

HTTP req handling with Hooks

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

Also, read An Interesting Guide to Exploring Flutter Drawer

Leave a Comment