The flutter_hooks package provides an innovative way to manage widget state and lifecycle in Flutter. Hooks are functions that allow you to 'hook into' Flutter's widget lifecycle, enabling you to easily manage state and side effects. If you are familiar with React, the concept of Hooks might sound similar. Hooks in Flutter enable you to write concise, maintainable, and reusable code, especially when dealing with state or lifecycle interactions. This article will discuss how to effectively use the flutter_hooks package, focusing on the Primitives—a set of low-level hooks that interact directly with various widget lifecycles.
What Are Flutter Hooks?
flutter_hooks is a package that allows you to manage stateful logic in a more functional manner than traditional StatefulWidgets. Rather than defining separate classes to manage widget state, hooks allow you to handle state within stateless functions. Hooks make your code more readable, easier to maintain, and more reusable across different components.
Getting Started with flutter_hooks
To use flutter_hooks, you need to add it to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
flutter_hooks: ^0.18.0
After adding it, run flutter pub get to fetch the package.
Understanding Primitive Hooks
Primitives are low-level hooks that interact with different lifecycles of a widget. These hooks allow you to perform certain actions when a widget’s lifecycle changes, making them ideal for tasks like managing subscriptions, disposing of resources, or running side effects. For example, useEffect can be used to manage a network subscription, automatically starting it when the widget is created and closing it when the widget is disposed of.
Here are some of the most commonly used primitive hooks:
useEffectuseCallbackuseStateuseMemoizeduseContextuseRefuseListenable
These hooks provide granular control over widget lifecycles, allowing you to react appropriately when a widget is created, updated, or disposed of.
Using Primitive Hooks: A Practical Example
Consider an example of creating a timer that updates the UI every second. Using StatefulWidgets can involve a lot of boilerplate code, such as managing initState, dispose, and calling setState to update the UI. In contrast, hooks allow you to manage state and side effects directly in the build function, reducing complexity and improving readability.
Example: Using useState and useEffect Hooks
We’ll use useState to hold the timer value and useEffect to handle the lifecycle of starting and stopping the timer.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class TimerWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final timerValue = useState(0);
useEffect(() {
final timer = Timer.periodic(Duration(seconds: 1), (timer) {
timerValue.value++;
});
// Dispose timer when the widget is removed from the widget tree
return timer.cancel;
}, []); // Empty dependency list, runs once when the widget is mounted
return Scaffold(
appBar: AppBar(title: Text('Timer with Hooks')),
body: Center(
child: Text(
'Timer Value: ${timerValue.value}',
style: TextStyle(fontSize: 24),
),
),
);
}
}
In the above example:
-
useStateis used to store and update the timer value. -
useEffectis used to initiate the timer when the widget is first built and cancel it when the widget is disposed of.
This approach is much more concise compared to using a StatefulWidget and managing initState, dispose, and setState calls.
Exploring More Primitive Hooks
useMemoized
The useMemoized hook allows you to memoize a computed value, meaning it stores the result of an expensive calculation and only recomputes it if its dependencies change. This can optimize performance by preventing unnecessary recalculations, especially when dealing with complex computations or large datasets.
For example, consider a widget that displays a value calculated using an expensive function:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
int computeExpensiveValue() {
// Simulating an expensive computation
return List.generate(100000, (i) => i).fold(0, (a, b) => a + b);
}
class ExpensiveComputationWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final expensiveValue = useMemoized(() => computeExpensiveValue(), []);
return Scaffold(
appBar: AppBar(title: Text('Expensive Computation with useMemoized')),
body: Center(
child: Text(
'Computed Value: $expensiveValue',
style: TextStyle(fontSize: 24),
),
),
);
}
}
In this example, computeExpensiveValue() is called only once when the widget is first built, thanks to useMemoized. If the widget rebuilds without any change in dependencies, the stored value will be reused, avoiding redundant recalculations.
useCallback
The useCallback hook allows you to create functions that are memoized. This is particularly helpful when you want to prevent unnecessary re-creations of functions during rebuilds, which can improve performance.
final increment = useCallback(() {
// Function logic here
}, [dependencies]);
By using useCallback, you ensure that the function is only recreated if its dependencies change.
useRef
The useRef hook allows you to maintain a reference to an object that doesn’t trigger rebuilds when updated. This is particularly useful for cases where you want to hold an object that changes frequently but doesn’t need to impact the UI directly.
final counterRef = useRef(0);
counterRef.value++;
This hook can be used when managing resources like controllers, scroll positions, or keeping a reference to previous values.
useListenable
useListenable is used when you want to work with a Listenable such as AnimationController or ValueNotifier. It helps to automatically start and dispose of listeners within the widget lifecycle.
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class AnimationExample extends HookWidget {
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
useEffect(() {
animationController.repeat();
return animationController.dispose;
}, [animationController]);
return Scaffold(
appBar: AppBar(title: Text('Animation with Hooks')),
body: Center(
child: FadeTransition(
opacity: animationController,
child: FlutterLogo(size: 150),
),
),
);
}
}
In this example, useAnimationController and useEffect work together to manage the lifecycle of the animation, ensuring it starts when the widget is built and disposes properly when removed.
useContext
The useContext hook provides direct access to the BuildContext of the widget. This can be useful for accessing inherited widgets or navigating to a different route. Here's a simple example of how useContext can be used to show a dialog:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class ContextExample extends HookWidget {
@override
Widget build(BuildContext context) {
final currentContext = useContext();
return Scaffold(
appBar: AppBar(title: Text('useContext Example')),
body: Center(
child: ElevatedButton(
onPressed: () {
showDialog(
context: currentContext,
builder: (context) => AlertDialog(
title: Text('Hello'),
content: Text('This is a dialog using useContext.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close'),
),
],
),
);
},
child: Text('Show Dialog'),
),
),
);
}
}
In this example, useContext allows direct access to the widget's context, which is then used to show an alert dialog when the button is pressed.
Hooks with Cubit, freezed, and injectable
We can also combine Hooks with popular state management solutions like Cubit, freezed, and injectable to create highly maintainable and readable code.
Example: Counter App with Cubit, freezed, and injectable
Let’s create a Counter app where we manage state using Cubit and inject dependencies using injectable.
Step 1: Set Up Cubit State Using freezed
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'counter_state.freezed.dart';
@freezed
class CounterState with _$CounterState {
const factory CounterState.initial() = Initial;
const factory CounterState.value(int count) = Value;
}
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState.initial());
void increment() {
state.mapOrNull(
value: (value) => emit(CounterState.value(value.count + 1)),
orElse: () => emit(CounterState.value(1)),
);
}
}
Step 2: Using injectable to Inject Cubit
To handle dependency injection, annotate the CounterCubit class with @injectable.
import 'package:injectable/injectable.dart';
@injectable
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState.initial());
void increment() {
state.mapOrNull(
value: (value) => emit(CounterState.value(value.count + 1)),
orElse: () => emit(CounterState.value(1)),
);
}
}
Step 3: Use the Cubit in a Widget Using Hooks
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'counter_cubit.dart';
import 'package:injectable/injectable.dart';
class CounterApp extends HookWidget {
@override
Widget build(BuildContext context) {
final counterCubit = useMemoized(() => getIt<CounterCubit>(), []);
return BlocBuilder<CounterCubit, CounterState>(
bloc: counterCubit,
builder: (context, state) {
final count = state.maybeWhen(
value: (count) => count,
orElse: () => 0,
);
return Scaffold(
appBar: AppBar(title: Text('Counter with Cubit, Hooks, and DI')),
body: Center(
child: Text(
'Counter: $count',
style: TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: counterCubit.increment,
child: Icon(Icons.add),
),
);
},
);
}
}
Conclusion
The flutter_hooks package offers a powerful way to work with the Flutter widget lifecycle by providing a functional approach to state management. Using low-level hooks like useState, useEffect, useCallback, useMemoized, and others allows you to significantly reduce boilerplate code while keeping your Flutter applications simple and easy to maintain.
By combining Hooks with state management solutions like Cubit and freezed, and dependency injection tools like injectable, you can create modern, scalable, and maintainable Flutter applications. Try experimenting with Hooks in a small CRUD app to see firsthand how they simplify state management and improve your development workflow. Give Hooks a try in your next project to see how they can improve your workflow!

Top comments (0)