Exploring Flutter Hooks and Their Integration with Riverpod

Nima Farzin
5 min readJun 28, 2024

--

Understanding Flutter Hooks

What Are Hooks?

Hooks are utilities from the flutter_hooks package, designed to simplify state management in Flutter. Originating from React, hooks are functions used inside widgets to manage state and side effects. They offer an alternative to StatefulWidget, making the code more reusable and composable.

Should You Use Hooks?

Hooks are powerful but not essential for every Flutter developer. They come with a learning curve and are not a core Flutter concept, which might make them feel out of place. If you’re new to Flutter or state management, you might want to start without hooks and consider them later as you gain more experience but stay with me and I show you how to simply understand and use them.

Benefits of Hooks

Hooks provide several advantages:

  • Reusability: They allow for extracting and reusing logic across different widgets.
  • Readability: By reducing widget nesting, hooks make the code cleaner.
  • Memory Management: Hooks automatically handle the creation and disposal of resources, reducing the risk of memory leaks.

Common Use Cases

Hooks are typically used for managing stateful UI objects, such as TextEditingController and AnimationController. They can also replace builder widgets like FutureBuilder or TweenAnimationBuilder, improving readability.

Example: Implementing a Fade-In Animation

Without hooks, you would use a StatefulWidget:

class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);

final Widget child;

@override
State<FadeIn> createState() => _FadeInState();
}

class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);

@override
void initState() {
super.initState();
animationController.forward();
}

@override
void dispose() {
animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}

With hooks, the equivalent code is simpler:

class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);

final Widget child;

@override
Widget build(BuildContext context) {
// Create an AnimationController. The controller will automatically be
// disposed when the widget is unmounted.
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);

// useEffect is the equivalent of initState + didUpdateWidget + dispose.
// The callback passed to useEffect is executed the first time the hook is
// invoked, and then whenever the list passed as second parameter changes.
// Since we pass an empty const list here, that's strictly equivalent to `initState`.
useEffect(() {
// start the animation when the widget is first rendered.
animationController.forward();
// We could optionally return some "dispose" logic here
return null;
}, const []);

// Tell Flutter to rebuild this widget when the animation updates.
// This is equivalent to AnimatedBuilder
useAnimation(animationController);

return Opacity(
opacity: animationController.value,
child: child,
);
}
}

or a better one


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

class FadeInAnimation extends HookWidget {
const FadeInAnimation({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 300),
});

final Widget child;
final Duration duration;

@override
Widget build(BuildContext context) {
final fade = useFadeAnimation(duration: duration);
return Opacity(
opacity: fade,
child: child,
);
}
}

double useFadeAnimation({
Curve curve = Curves.easeInOut,
Duration duration = const Duration(milliseconds: 800),
}) {
final animationController = useAnimationController(duration: duration);
final fadeAnimation = useAnimation(
CurvedAnimation(parent: animationController, curve: curve),
);

useEffect(() {
animationController.forward();
return null;
}, []);

return fadeAnimation;
}

Or combining more animations

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

class FadeInScaleAnimation extends HookWidget {
const FadeInScaleAnimation({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 300),
this.beginScale = 0.8,
});

final Widget child;
final Duration duration;
final double beginScale;

@override
Widget build(BuildContext context) {
final fade = useFadeAnimation(duration: duration);
final scaleAnimation = useScaleAnimation(
beginScale: beginScale,
duration: duration,
);

return Opacity(
opacity: fade,
child: Transform.scale(
scale: scaleAnimation,
child: child,
),
);
}
}

// separate reusable function
double useFadeAnimation({
Curve curve = Curves.easeInOut,
Duration duration = const Duration(milliseconds: 800),
}) {
final animationController = useAnimationController(duration: duration);
final fadeAnimation = useAnimation(
CurvedAnimation(parent: animationController, curve: curve),
);

useEffect(() {
animationController.forward();
return null;
}, []);

return fadeAnimation;
}

// separate reusable function
double useScaleAnimation({
double beginScale = 0.8,
Curve curve = Curves.easeInOut,
Duration duration = const Duration(milliseconds: 800),
}) {
final animationController = useAnimationController(duration: duration);
final scaleAnimation = useAnimation(
Tween<double>(begin: beginScale, end: 1.0).animate(
CurvedAnimation(parent: animationController, curve: curve),
),
);

useEffect(() {
animationController.forward();
return null;
}, []);

return scaleAnimation;
}

Rules of Hooks

Hooks have specific constraints:

  • Inside HookWidget: Hooks can only be used inside the build method of a widget that extends HookWidget.
  • No Conditional Use: Hooks must not be used conditionally or inside loops.
  • If state management like Riverpod is for “global” application state, hooks are for local widget state

Good Example

class Example extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useAnimationController();
return Container();
}
}

Bad Examples

Using hooks inside a non-HookWidget:

class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = useAnimationController(); // Error
return Container();
}
}

Using hooks conditionally:

class Example extends HookWidget {
final bool condition;
const Example({required this.condition, super.key});

@override
Widget build(BuildContext context) {
if (condition) {
final controller = useAnimationController(); // Error
}
return Container();
}
}

For more information about hooks, refer to the flutter_hooks documentation.

Integrating Hooks with Riverpod

Combining Hooks and Riverpod

Hooks and Riverpod can be used together to enhance state management in Flutter applications. Although they are separate packages, hooks_riverpod provides a way to combine the functionality of both.

Installation

To use hooks with Riverpod, install both flutter_hooks and hooks_riverpod packages:

dependencies:
flutter_hooks:
hooks_riverpod:

Using HookConsumerWidget

HookConsumerWidget is a special widget that combines HookWidget and ConsumerWidget, allowing you to use both hooks and Riverpod providers in the same widget:

class Example extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
}
}

Using Builders

Alternatively, you can use the builders provided by both packages:

class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
return HookBuilder(builder: (context) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
});
},
);
}
}

Streamlined Approach with HookConsumer

hooks_riverpod provides HookConsumer, combining both Consumer and HookBuilder:

class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HookConsumer(
builder: (context, ref, child) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
},
);
}
}

Conclusion

Using hooks and Riverpod together provides a powerful combination for managing state in Flutter applications. Hooks offer a more readable and reusable way to manage local state, while Riverpod provides robust global state management. By leveraging both, you can create clean, efficient, and maintainable Flutter code.

Resources

https://pub.dev/packages/flutter_hooks
https://riverpod.dev/docs/concepts/about_hooks

You can find me on LinkedIn and GitHub

www.linkedin.com/in/nimafarzin-pr

https://github.com/nimafarzin-pr

--

--