Advanced Flutter Hooks

Advanced Flutter Hooks

You can find the introduction to Flutter hooks here: Flutter Hooks, everything to know about them

Now today we are going to look at few advanced concepts using Flutter Hooks

useFuture:

Example: Asynchronous Data Fetching with useFuture

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class DataFetchPage extends HookWidget {
  const DataFetchPage({super.key});

  Future<String> fetchData() async {
    await Future.delayed(const Duration(seconds: 2));
    return 'Fetched data from the server';
  }

  @override
  Widget build(BuildContext context) {
    final future = useMemoized(() => fetchData());
    final snapshot = useFuture(future);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Data Fetch using Hooks'),
      ),
      body: snapshot.connectionState == ConnectionState.waiting
          ? const CircularProgressIndicator()
          : snapshot.hasError
              ? Text('Error: ${snapshot.error}')
              : Text('Data : ${snapshot.data}'),
    );
  }
}

In here we have used two features of Hooks

  1. useMemoized: Memorizes the future to avoid refetching data on every build. So it basically caches the future object.

  2. useFuture: Manages the state of the future, providing connection states and results.

Remember: useFuture doesn't persist the Future in memory which is why useMemoized is needed to store the data.

You can find the code here: hooks_useFuture

Combining Hooks

Example: Create a Form that uses multiple Hooks

Our Form will use useState, useTextEditingController and useEffect

class CombinedHooksForm extends HookWidget {
  const CombinedHooksForm({super.key});

  @override
  Widget build(BuildContext context) {
    final _formKey = useMemoized(() => GlobalKey<FormState>());
    final _emailController = useTextEditingController();
    final _passwordController = useTextEditingController();
    final _isSubmitting = useState(false);

    useEffect(() {
      if (_isSubmitting.value) {
        // Let's do a Mock API call

        Future.delayed(const Duration(seconds: 2)).then((_) {
          _isSubmitting.value = false;
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Form Submitted'),
            ),
          );
        });
      }
    }, [_isSubmitting.value]);

    void _submitForm() {
      if (_formKey.currentState!.validate()) {
        _isSubmitting.value = true;
      }
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Combined Hooks Form'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
                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;
                },
              ),
              TextFormField(
                controller: _passwordController,
                decoration: const InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your password';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 20),
              _isSubmitting.value
                  ? const CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _submitForm,
                      child: const Text('Submit'),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

In the above class we have used the following:

  1. useTextEditingController: Manages controllers for email and password fields.

  2. useState: Tracks the submission state.

  3. useEffect: Handles side effects when the form is submitted, such as showing a loading indicator and a success message.

You can find the source code for the above tutorial here: combine_hooks

Custom Hooks:

Example: Create a Timer that works from a Custom Hook

We are creating a timer that works from a custom hook

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class TimerScreen extends HookWidget {
  const TimerScreen({super.key});

  @override
  Widget build(BuildContext context) {
    int useTimer() {
      final time = useState(0);

      useEffect(() {
        final timer =
            Timer.periodic(const Duration(seconds: 1), (timer) => time.value++);
        return () => timer.cancel();
      }, []);

      return time.value;
    }

    final time = useTimer();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Timer using Custom Hook'),
      ),
      body: Center(
        child: Text('Timer: $time'),
      ),
    );
  }
}

In the above example we have used:

  1. useState: Manages the timer state.

  2. useEffect: Starts and cleans up the timer.

  3. Custom Hook: useTimer encapsulates the logic for the timer, making it reusable.

You can find the source code for custom hooks here: custom_hooks

Pagination:

Example: Fetching and Displaying List Data with Pagination

We'll create an example that fetches paginated data from an API and displays it in a list.

class PaginatedListScreen extends HookWidget {
  const PaginatedListScreen({super.key});

  Future<List<String>> fetchPage(int page) async {
    await Future.delayed(
        const Duration(seconds: 2)); // Simulating network delay

    return List.generate(10, (index) => 'Item ${page * 10 + index + 1}');
  }

  @override
  Widget build(BuildContext context) {
    final _page = useState(0);
    final _items = useState<List<String>>([]);
    final _isFetching = useState(false);

    void _fetchNextPage() async {
      if (_isFetching.value) return;

      _isFetching.value = true;
      final newItems = await fetchPage(_page.value);

      // We are using a spread operator to combine both arrays into a new array
      _items.value = [..._items.value, ...newItems];

      // Once we get the value. We have to increase the page number
      _page.value++;

      //Finally after fetching is done. We have to set _isFetching to false
      _isFetching.value = false;
    }

    useEffect(() {
      _fetchNextPage();
    }, []);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Paginated List'),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
            _fetchNextPage();
          }
          return true;
        },
        child: ListView.builder(
          itemCount: _items.value.length,
          itemBuilder: (context, index) {
            if (index == _items.value.length) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
            return ListTile(
              title: Text(_items.value[index]),
            );
          },
        ),
      ),
    );
  }
}

In the above example:

  1. useState: Manages the current page, items, and fetching state.

  2. useEffect: Fetches the first page of data when the component is mounted.

  3. NotificationListener: Detects when the user scrolls to the bottom of the list and triggers the next page fetch.

You can find the source code for the above tutorial here: paginated_list

These examples demonstrate how to use Flutter hooks for more advanced use cases. Experiment with these patterns and adapt them to your specific needs.

Source Code:

As always you can find the source code here: https://github.com/khkred/learn_flutter_hooks

Did you find this article valuable?

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