Funciones de Ciclo de Vida
callOnce() y onDispose()
Ejecuta una función solo en la primera construcción (incluso en un StatelessWidget), con handler de dispose opcional.
Firmas del método:
void callOnce(
void Function(BuildContext context) init,
{void Function()? dispose}
);
void onDispose(void Function() dispose);Caso de uso típico: Disparar carga de datos en la primera construcción, luego mostrar resultados con 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()
Ejecuta un callback una vez después de que se complete la construcción actual. A diferencia de callOnce() que se ejecuta inmediatamente durante la construcción, esto se ejecuta en un callback post-frame.
Firma del método:
void callOnceAfterThisBuild(
void Function(BuildContext context) callback
);Perfecto para:
- Navegación después de que las dependencias async estén listas
- Mostrar diálogos o snackbars después del renderizado inicial
- Acceder a dimensiones de RenderBox
- Operaciones que no deberían ejecutarse durante la construcción
Comportamiento clave:
- Se ejecuta una vez después de la primera construcción donde se llama esta función
- Se ejecuta en un callback post-frame (después de layout y paint)
- Seguro de usar dentro de condicionales - se ejecutará una vez cuando la condición primero se vuelva verdadera
- No se ejecutará nuevamente en construcciones subsiguientes, incluso si se llama nuevamente
Ejemplo - Navegar cuando las dependencias estén listas:
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...'),
],
),
),
);
}
}Contraste con callOnce:
callOnce(): Se ejecuta inmediatamente durante la construcción (síncrono)callOnceAfterThisBuild(): Se ejecuta después de que la construcción se complete (callback post-frame)
callAfterEveryBuild()
Ejecuta un callback después de cada construcción. El callback recibe una función cancel() para detener invocaciones futuras.
Firma del método:
void callAfterEveryBuild(
void Function(BuildContext context, void Function() cancel) callback
);Casos de uso:
- Actualizar posición de scroll después de reconstrucciones
- Reposicionar overlays o tooltips
- Realizar mediciones después de cambios de layout
- Sincronizar animaciones con estado de reconstrucción
Ejemplo - Scroll al principio con 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),
),
);
}
}Importante:
- El callback se ejecuta después de CADA reconstrucción
- Usa
cancel()para detener cuando ya no sea necesario - Se ejecuta en callback post-frame (después de que el layout se complete)
createOnce y createOnceAsync
Crea un objeto en la primera construcción que se dispone automáticamente cuando el widget se destruye. Ideal para todos los tipos de controllers (TextEditingController, AnimationController, ScrollController, etc.) o estado reactivo local (ValueNotifier, ChangeNotifier).
Firmas del método:
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'),
),
],
);
}
}Cómo funciona:
- En la primera construcción, el objeto se crea con
factoryFunc - En construcciones subsiguientes, se retorna la misma instancia
- Cuando el widget se dispone:
- Si el objeto tiene un método
dispose(), se llama automáticamente - Si necesitas una función de dispose diferente (como
cancel()en StreamSubscription), pásala como el parámetrodispose
- Si el objeto tiene un método
Crear estado local con 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 para llamadas de función async de una sola vez para mostrar datos, por ejemplo desde algún endpoint backend.
Firma completa:
AsyncSnapshot<T> createOnceAsync<T>(
Future<T> Function() factoryFunc,
{required T initialValue, void Function(T)? dispose}
);Cómo funciona:
- Retorna
AsyncSnapshot<T>inmediatamente coninitialValue - Ejecuta
factoryFuncasincrónicamente en la primera construcción - El widget se reconstruye automáticamente cuando el future se completa
AsyncSnapshotcontiene el estado (loading, data, error)- El objeto se dispone cuando el widget se destruye
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');
}
}