Skip to content

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:

dart
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:

dart
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:

dart
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:

dart
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:

dart
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:

dart
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:

dart
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}
);
dart
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ámetro dispose

Crear estado local con ValueNotifier:

dart
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:

dart
AsyncSnapshot<T> createOnceAsync<T>(
  Future<T> Function() factoryFunc,
  {required T initialValue, void Function(T)? dispose}
);

Cómo funciona:

  • Retorna AsyncSnapshot<T> inmediatamente con initialValue
  • Ejecuta factoryFunc asincrónicamente en la primera construcción
  • El widget se reconstruye automáticamente cuando el future se completa
  • AsyncSnapshot contiene el estado (loading, data, error)
  • El objeto se dispone cuando el widget se destruye
dart
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');
  }
}

Publicado bajo la Licencia MIT.