LeanCode Hooks
A curated collection of Flutter hooks that we commonly use at LeanCode, all gathered in one place for better discoverability and consistent versioning.
Installation
Add the package to your pubspec.yaml:
dependencies:
leancode_hooks: ^0.1.2
This package exports package:flutter_hooks, so you don’t need to add it as a separate dependency.
Import
import 'package:leancode_hooks/leancode_hooks.dart';
Available Hooks
BLoC Integration
useBloc
Provides a Cubit or Bloc that is automatically disposed without having to use BlocProvider.
Signature:
B useBloc<B extends BlocBase<Object?>>(
B Function() create, {
List<Object?> keys = const [],
})
Example:
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final counterCubit = useBloc(() => CounterCubit());
final count = useBlocState(counterCubit);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => counterCubit.increment(),
child: Text('Increment'),
),
],
);
}
}
useBlocState
Provides the current state of a Cubit or Bloc and forces the widget to rebuild on state changes.
Signature:
S useBlocState<S>(BlocBase<S> bloc)
Example:
final authBloc = context.read<AuthBloc>();
final authState = useBlocState(authBloc);
if (authState is Authenticated) {
return HomeScreen();
}
return LoginScreen();
useBlocListener
Takes a Bloc or Cubit and invokes a listener in response to state changes.
Signature:
void useBlocListener<S>({
required BlocBase<S> bloc,
required void Function(S state) listener,
})
Example:
final authBloc = context.read<AuthBloc>();
useBlocListener<AuthState>(
bloc: authBloc,
listener: (state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
);
Debouncing & Throttling
useDebounce
Returns a function whose invocation will be delayed by the specified duration. When called again, the previous invocation is canceled.
Signature:
DebouncedCallback useDebounce(Duration duration)
Example:
final debounce = useDebounce(Duration(milliseconds: 500));
final searchController = useTextEditingController();
useEffect(() {
void listener() {
debounce(() {
// This will only run 500ms after the user stops typing
performSearch(searchController.text);
});
}
searchController.addListener(listener);
return () => searchController.removeListener(listener);
}, [searchController]);
useThrottle
Returns a function that is throttled for the specified duration after being called.
Signature:
ThrottledCallback useThrottle(Duration duration)
Example:
final throttle = useThrottle(Duration(seconds: 1));
return ElevatedButton(
onPressed: () {
throttle(() {
// This will run at most once per second
submitForm();
});
},
child: Text('Submit'),
);
Text Editing
useDeclarativeTextEditingController
A wrapper around useTextEditingController that updates the controller’s text when the value changes declaratively.
Signature:
TextEditingController useDeclarativeTextEditingController({
required String text,
})
Example:
final searchQuery = useState('');
final controller = useDeclarativeTextEditingController(
text: searchQuery.value,
);
return Column(
children: [
TextField(controller: controller),
ElevatedButton(
onPressed: () => searchQuery.value = 'Flutter',
child: Text('Set to "Flutter"'),
),
],
);
This hook handles updating the controller’s text when the text parameter changes, which is useful for controlled text inputs.
useSyncedTextEditingController
A wrapper around useTextEditingController that makes it easier to add an onChanged callback.
Signature:
TextEditingController useSyncedTextEditingController(
void Function(TextEditingValue value) onChanged, {
String? initialText,
})
Example:
final searchQuery = useState('');
final controller = useSyncedTextEditingController(
(value) => searchQuery.value = value.text,
initialText: '',
);
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Search',
suffixText: 'Query: ${searchQuery.value}',
),
);
Utilities
useDisposable
Creates and memoizes an instance, then executes a dispose function on unmount to cleanup resources.
Signature:
T useDisposable<T>({
required ValueGetter<T> builder,
required void Function(T) dispose,
List<Object?> keys = const [],
})
Example:
final videoController = useDisposable(
builder: () => VideoPlayerController.network('https://example.com/video.mp4'),
dispose: (controller) => controller.dispose(),
);
return VideoPlayer(videoController);
useFocused
Subscribes to FocusNode changes and returns whether the node has focus.
Signature:
bool useFocused(FocusNode focusNode)
Example:
final focusNode = useFocusNode();
final isFocused = useFocused(focusNode);
return TextField(
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(
borderSide: BorderSide(
color: isFocused ? Colors.blue : Colors.grey,
width: isFocused ? 2 : 1,
),
),
),
);
usePostFrameEffect
Registers an effect to be run in WidgetsBinding.addPostFrameCallback.
Signature:
void usePostFrameEffect(
VoidCallback effect, {
List<Object?> keys = const [],
})
Example:
final scrollController = useScrollController();
usePostFrameEffect(
() {
// Scroll to bottom after the frame is rendered
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
},
[messages.length], // Re-run when messages change
);
useStreamListener
Listens to a stream and calls onData on new data.
Signature:
void useStreamListener<T>(
Stream<T> stream,
ValueChanged<T> onData, {
Function? onError,
VoidCallback? onDone,
bool? cancelOnError,
List<Object?> keys = const [],
})
Example:
final locationStream = locationService.getLocationStream();
useStreamListener<Location>(
locationStream,
(location) {
print('New location: ${location.latitude}, ${location.longitude}');
},
onError: (error) => print('Location error: $error'),
);
useTapGestureRecognizer
Creates a TapGestureRecognizer that will be disposed automatically.
Signature:
TapGestureRecognizer useTapGestureRecognizer(
TapGestureRecognizer Function() builder, {
List<Object?> keys = const [],
})
Example:
final tapRecognizer = useTapGestureRecognizer(
() => TapGestureRecognizer()
..onTap = () => print('Tapped!'),
);
return Text.rich(
TextSpan(
text: 'Click ',
children: [
TextSpan(
text: 'here',
style: TextStyle(color: Colors.blue),
recognizer: tapRecognizer,
),
TextSpan(text: ' to continue'),
],
),
);
Best Practices
Use hooks only in HookWidget
All hooks must be called inside a HookWidget or HookConsumer (from flutter_hooks).
Maintain hook call order
Always call hooks in the same order on every build. Don’t call hooks conditionally or in loops.
Specify keys for dependencies
When using hooks with memoization (like useBloc, useDisposable), pass the keys parameter to control when the value is recreated.
Clean up resources
Use useDisposable for resources that need cleanup, or useEffect with a cleanup function.
Source Code
View the source code on GitHub.