Skip to content

callOnce & createOnce

callOnce() and onDispose()

Execute a function only on the first build (even in a StatelessWidget), with optional dispose handler.

Method signatures:

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

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...');
  }
}

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:

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'),
        ),
      ],
    );
  }
}

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 the dispose parameter

Creating local state with 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 for one-time async function calls to display data, for instance from some backend endpoint.

Full signature:

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

How it works:

  • Returns AsyncSnapshot<T> immediately with initialValue
  • Executes factoryFunc asynchronously on first build
  • Widget rebuilds automatically when the future completes
  • AsyncSnapshot contains the state (loading, data, error)
  • Object is disposed when widget is destroyed
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');
  }
}

Released under the MIT License.