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
useMemoized: Memorizes the future to avoid refetching data on every build. So it basically caches the future object.
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:
useTextEditingController: Manages controllers for email and password fields.
useState: Tracks the submission state.
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:
useState: Manages the timer state.
useEffect: Starts and cleans up the timer.
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:
useState: Manages the current page, items, and fetching state.
useEffect: Fetches the first page of data when the component is mounted.
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