Why utopia_hooks
A Flutter screen has to do four things: hold local state, read shared app state, run async work, and stay testable. The framework gives you one tool for the first job - StatefulWidget - and leaves the rest to a patchwork of other libraries. The result is a presentation layer assembled from parts that don't share a model.
StatefulWidget is where it starts. State lives in a second class reached through createState; you read a field, then call setState to write it; lifecycle splits across initState and dispose; reacting to changed inputs means a hand-written didUpdateWidget. None of that logic can be unit-tested without pumping a widget tree, so it tends to stay in build, where it tangles with the UI.
Shared state is a separate decision - a provider package, a stream, a singleton - with its own way of being read and its own lifecycle. Async loading is a third: FutureBuilder, a manual mounted check, a bool isLoading you wire up by hand. Each concern is a different shape, and gluing them together is the work.
utopia_hooks is one model for all four. A hook is a composable unit of state and logic, called from build. The same mechanism that holds a counter reads global state, drives an async fetch, and runs under a plain unit test with no widget tree. Local state, shared state, async, and testable logic stop being four problems with four libraries and become one.
Same feature, two models
Here is the smallest version of the difference: a counter. Flip the toggle to compare.
- utopia_hooks
- Vanilla Flutter
class CounterScreen extends HookWidget {
const CounterScreen({super.key});
Widget build(BuildContext context) {
final count = useState(0);
return Scaffold(
body: Center(child: Text('Count: ${count.value}')),
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++, // <- the write schedules the rebuild
child: const Icon(Icons.add),
),
);
}
}
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0; // <- the mutable field, in a second class
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Count: $_count')),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _count++), // <- mutate, then rebuild
child: const Icon(Icons.add),
),
);
}
}
One class instead of two, no createState, and the state declaration sits next to where it is used. The payoff grows with the screen: initState/dispose collapse into a single useEffect whose teardown can't drift from its setup, and didUpdateWidget becomes a keys array that re-runs work only when an input changes. The From StatefulWidget guide walks the full mapping.
What you get
Composability. Hooks call other hooks. useFuture is useState plus an effect; a paginated list is a few hooks stacked together. You build a screen's logic by combining small pieces, and you factor a repeated combination into a custom hook the same way you factor a function - no base class, no mixin.
One model for local and global state. Shared state is a hook too. Register it once at the app root, then read it anywhere with useProvided; it rebuilds only the hooks that read it. Local state and app-wide state are written and consumed the same way, so moving a value from one scope to the other is a small change, not a rewrite onto a different library. See Global state.
Testable logic, no widget tree. Because a hook is just a function over a context, it runs in a unit test without Flutter. Wrap it in a SimpleHookContext and assert on the value it produces:
final context = SimpleHookContext(useCounterState);
expect(context().value, 0);
context().onIncrement();
expect(context().value, 1);
The logic that would otherwise be trapped in build becomes a plain function you can drive directly - and dependencies can be faked through the same context.
These three add up to the architecture the rest of the docs build on: the Screen / State / View pattern, where a screen is a State class, a hook that builds it, and a View that renders it - logic and UI cleanly apart, every part of it testable.
See also
- Getting Started - install the package and run the first hook
- Basics - the three core hooks and the rules for calling them
- Screen / State / View - the architecture these hooks are built for
- Comparisons - how utopia_hooks compares to StatefulWidget, BLoC, Riverpod, flutter_hooks, and React
Crafted for you by UtopiaSoftware 👾