Skip to main content

useListenable

Subscribes the hook to a Listenable and rebuilds when it notifies. Use it to bring an existing Flutter Listenable (an AnimationController, ScrollController, ChangeNotifier, TabController, ...) into a hook so changes drive a rebuild.

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


Widget build(BuildContext context) {
final controller = useScrollController(); // <- A ScrollController is a Listenable

useListenable(controller); // <- Rebuild whenever the controller notifies

return Scaffold(
appBar: AppBar(
title: Text('Offset: ${controller.offset.toStringAsFixed(0)}'), // <- Read off the source
),
body: ListView.builder(
controller: controller,
itemCount: 100,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
),
);
}
}

Signature

void useListenable(Listenable? listenable, {bool Function()? shouldRebuild});

The listenable is nullable - passing null subscribes to nothing. useListenable returns nothing: it only registers the subscription, so you keep reading the current value from the Listenable itself. shouldRebuild is called on every notification; return false to skip the rebuild. Without it, every notification rebuilds.

Use cases

  • Reacting to a Flutter controller that is not a ValueListenable (e.g. TabController, ScrollController) where the interesting state is read off the controller directly.
  • Subscribing to a ChangeNotifier whose fields you read yourself.
  • Filtering out noisy notifications with shouldRebuild so the hook rebuilds only when it matters.

Caveats

  • useListenable does not return a value - it is purely the subscription. Read the data from the listenable you passed in.

    // DON'T
    final value = useListenable(controller); // <- Returns void, not a value

    // DO
    useListenable(controller);
    final value = controller.index; // <- Read from the source
  • For a Listenable that exposes a single value (a ValueListenable or a ListenableValue), prefer useValueListenable / useListenableValue below - they subscribe and return the value in one call, and skip rebuilds when the value is unchanged.

useValueListenable

Subscribes to a Flutter ValueListenable<T> and returns its current value. Rebuilds only when the value changes.

class TabTitleState {
final String title;

const TabTitleState({required this.title});
}

TabTitleState useTabTitleState(ValueListenable<int> selectedTab) {
// Subscribes to a Flutter ValueListenable and returns its current value.
// Rebuilds only when the value changes (default shouldRebuild is prev != curr).
final index = useValueListenable(selectedTab); // <- Read + subscribe in one call

return TabTitleState(title: 'Tab ${index + 1}');
}

Signature

T useValueListenable<T>(ValueListenable<T> listenable, {bool Function(T prev, T curr)? shouldRebuild});

Returns the listenable's current value. By default shouldRebuild is (prev, curr) => prev != curr, so a notification that does not change the value (by ==) triggers no rebuild. Pass your own to control this - for example, comparing only an identity field.

class UserBannerState {
final String userId;

const UserBannerState({required this.userId});
}

UserBannerState useUserBannerState(ValueListenable<User> user) {
// Only rebuild when the identity changes, ignoring unrelated field updates.
final current = useValueListenable<User>(
user,
shouldRebuild: (prev, curr) => prev.id != curr.id, // <- Skip rebuilds when id is unchanged
);

return UserBannerState(userId: current.id);
}

class User {
final String id;
final String name;

const User({required this.id, required this.name});
}

Use cases

  • Reading a ValueNotifier<T> or any Flutter ValueListenable<T>.
  • Rebuilding on identity changes only, ignoring unrelated field updates, via shouldRebuild.

Caveats

  • The default prev != curr check means a poorly implemented == operator can suppress rebuilds you expected. If equal values should still rebuild, pass shouldRebuild: (_, __) => true.

useListenableValue

The same as useValueListenable, but for utopia's ListenableValue<T> instead of Flutter's ValueListenable<T>.

class GreetingState {
final String greeting;
final void Function() onShout;

const GreetingState({required this.greeting, required this.onShout});
}

GreetingState useGreetingState(ListenableValue<String> name) {
// ListenableValue is utopia's lightweight ValueNotifier. A useState() result
// is already a ListenableValue, so it can be passed straight in.
final currentName = useListenableValue(name); // <- Rebuilds only when name changes

return GreetingState(
greeting: 'Hello, $currentName',
onShout: () {}, // wired by the caller
);
}

Signature

T useListenableValue<T>(ListenableValue<T> listenable, {bool Function(T prev, T curr)? shouldRebuild});

ListenableValue<T> is utopia_utils's lightweight counterpart to ValueNotifier - a Value<T> that is also a Listenable. A useState(...) result implements ListenableMutableValue<T>, so it is itself a ListenableValue<T>: you can pass state from one hook straight into useListenableValue in another. shouldRebuild behaves exactly as in useValueListenable (default prev != curr).

info

ListenableValue and Flutter's ValueListenable are interchangeable via extensions: .asValueListenable() and .asListenableValue(). Reach for whichever variant matches the type you already hold instead of converting.

Use cases

  • Consuming a ListenableValue produced elsewhere (a useState result, a useNotifiableValue, a value exposed by a global state).
  • The same identity-only rebuild trick as useValueListenable, on a ListenableValue source.

Caveats

  • Most local state already drives rebuilds on its own - you rarely call useListenableValue on your own useState. It earns its place when a value is handed in from outside the current hook.

See also