Lifecycle Functions
callOnce() and onDispose()
Execute a function only on the first build (even in a StatelessWidget), with optional dispose handler.
Method signatures:
void callOnce(
void Function(BuildContext context) init,
{void Function()? dispose}
);
void onDispose(void Function() dispose);Typical use case: Trigger data loading on first build, then display results with watchValue:
class UserWidget extends WatchingWidget {
const UserWidget({super.key});
@override
Widget build(BuildContext context) {
// Trigger data loading once on first build
callOnce((context) => di<UserDataService>().loadUser());
// Watch and display the loaded data
final user = watchValue((UserDataService s) => s.currentUser);
return Text(user?.name ?? 'Loading...');
}
}callOnceAfterThisBuild()
Execute a callback once after the current build completes. Unlike callOnce() which runs immediately during build, this runs in a post-frame callback.
Method signature:
void callOnceAfterThisBuild(
void Function(BuildContext context) callback
);Perfect for:
- Navigation after async dependencies are ready
- Showing dialogs or snackbars after initial render
- Accessing RenderBox dimensions
- Operations that should not run during build
Key behavior:
- Executes once after the first build where this function is called
- Runs in a post-frame callback (after layout and paint)
- Safe to use inside conditionals - will execute once when the condition first becomes true
- Won't execute again on subsequent builds, even if called again
Example - Navigate when dependencies are ready:
class InitializationScreen extends WatchingWidget {
@override
Widget build(BuildContext context) {
final dbReady = isReady<Database>();
final configReady = isReady<ConfigService>();
if (dbReady && configReady) {
// Navigate once when all dependencies are ready
// callOnceAfterThisBuild executes after the current build completes
// Safe for navigation, dialogs, and accessing RenderBox
callOnceAfterThisBuild((context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => MainApp()),
);
});
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
if (dbReady) Text('✓ Database ready'),
if (configReady) Text('✓ Configuration loaded'),
if (!dbReady || !configReady) Text('Initializing...'),
],
),
),
);
}
}Contrast with callOnce:
callOnce(): Runs immediately during build (synchronous)callOnceAfterThisBuild(): Runs after build completes (post-frame callback)
callAfterEveryBuild()
Execute a callback after every build. The callback receives a cancel() function to stop future invocations.
Method signature:
void callAfterEveryBuild(
void Function(BuildContext context, void Function() cancel) callback
);Use cases:
- Update scroll position after rebuilds
- Reposition overlays or tooltips
- Perform measurements after layout changes
- Sync animations with rebuild state
Example - Scroll to top with cancel:
class ScrollToTopWidget extends StatelessWidget with WatchItMixin {
final ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
final counter = watch(counterNotifier);
// Scroll to top after every build where counter changes
// The cancel function allows stopping the callback when needed
callAfterEveryBuild((context, cancel) {
if (counter.value > 5) {
// Stop calling this callback after counter reaches 5
cancel();
return;
}
// Scroll to top after each rebuild
if (scrollController.hasClients) {
scrollController.animateTo(
0,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
return Scaffold(
appBar: AppBar(title: Text('Scroll Example')),
body: ListView.builder(
controller: scrollController,
itemCount: 50,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index - Counter: ${counter.value}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: counterNotifier.increment,
child: Icon(Icons.add),
),
);
}
}Important:
- Callback executes after EVERY rebuild
- Use
cancel()to stop when no longer needed - Runs in post-frame callback (after layout completes)
createOnce and createOnceAsync
Create an object on the first build that is automatically disposed when the widget is destroyed. Ideal for all types of controllers (TextEditingController, AnimationController, ScrollController, etc.) or reactive local state (ValueNotifier, ChangeNotifier).
Method signatures:
T createOnce<T extends Object>(
T Function() factoryFunc,
{void Function(T)? dispose}
);
AsyncSnapshot<T> createOnceAsync<T>(
Future<T> Function() factoryFunc,
{required T initialValue, void Function(T)? dispose}
);class TextFieldWithClear extends WatchingWidget {
@override
Widget build(BuildContext context) {
final controller =
createOnce<TextEditingController>(() => TextEditingController());
return Row(
children: [
TextField(
controller: controller,
),
ElevatedButton(
onPressed: () => controller.clear(),
child: const Text('Clear'),
),
],
);
}
}How it works:
- On first build, the object is created with
factoryFunc - On subsequent builds, the same instance is returned
- When the widget is disposed:
- If the object has a
dispose()method, it's called automatically - If you need a different dispose function (like
cancel()on StreamSubscription), pass it as thedisposeparameter
- If the object has a
Creating local state with ValueNotifier:
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// Create a local notifier that persists across rebuilds
final counter = createOnce(() => ValueNotifier<int>(0));
// Watch it directly (not from get_it)
final count = watch(counter).value;
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}createOnceAsync
Ideal for one-time async function calls to display data, for instance from some backend endpoint.
Full signature:
AsyncSnapshot<T> createOnceAsync<T>(
Future<T> Function() factoryFunc,
{required T initialValue, void Function(T)? dispose}
);How it works:
- Returns
AsyncSnapshot<T>immediately withinitialValue - Executes
factoryFuncasynchronously on first build - Widget rebuilds automatically when the future completes
AsyncSnapshotcontains the state (loading, data, error)- Object is disposed when widget is destroyed
class UserDataWidget extends WatchingWidget {
const UserDataWidget({super.key});
@override
Widget build(BuildContext context) {
// Fetch data once on first build
final snapshot = createOnceAsync(
() => di<BackendService>().fetchUserData(),
initialValue: '',
);
// Display based on AsyncSnapshot state
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text(snapshot.data ?? 'No data');
}
}