postFrame static method

Future<void> postFrame(
  1. FutureOr<void> action(), {
  2. List<ScrollController> scrollControllers = const [],
  3. int maxWaitFrames = 5,
  4. bool waitForEndOfFrame = true,
  5. int endOfFramePasses = 2,
})

Schedule action to run after the current frame.

Parameters:

  • scrollControllers - A list of controllers whose metrics you depend on. Each controller is waited on until it has clients and its metrics appear stable for up to maxWaitFrames frame passes. Provide multiple controllers if your callback needs both (e.g. nested horizontal & vertical lists).
  • maxWaitFrames - Upper bound on frame waits for stabilization; protects against indefinite waiting if metrics never settle. A value of 0 skips metric waiting entirely.
  • waitForEndOfFrame - If true, awaits WidgetsBinding.endOfFrame for each of the computed endOfFramePasses passes before evaluating scroll controller metrics or executing the action.
  • endOfFramePasses - Number of additional end-of-frame completions to await. Clamped to the range 1..maxWaitFrames to avoid pointless waits.

The function tolerates layout changes that happen during the stabilization window: if scroll metrics change (extent or viewport size) the wait ends early since a change often implies metrics are now ready for use.

Implementation

static Future<void> postFrame(
  FutureOr<void> Function() action, {
  List<ScrollController> scrollControllers = const [],
  int maxWaitFrames = 5,
  bool waitForEndOfFrame = true,
  int endOfFramePasses = 2,
}) {
  final completer = Completer<void>();
  final binding = WidgetsBinding.instance;

  // Using addPostFrameCallback ensures we schedule AFTER first frame render.
  binding.addPostFrameCallback((_) async {
    try {
      // Optionally wait for extra end-of-frame passes.
      if (waitForEndOfFrame) {
        final passes = maxWaitFrames <= 0
            ? 0 // If we are not allowed frames, skip waiting entirely.
            : endOfFramePasses.clamp(1, maxWaitFrames);
        for (var i = 0; i < passes; i++) {
          await binding.endOfFrame; // Wait for pipeline to fully drain.
        }
      }

      // Wait for scroll controller metrics stabilization.
      for (final controller in scrollControllers) {
        await _waitForControllerMetrics(
          controller,
          maxWaitFrames,
          endOfFramePasses,
        );
      }

      // Execute user callback once; Future.sync preserves sync errors.
      if (!completer.isCompleted) {
        await Future.sync(action);
        completer.complete();
      }
    } catch (error, stackTrace) {
      // Propagate errors to the returned future for testability and chaining.
      if (!completer.isCompleted) {
        completer.completeError(error, stackTrace);
      }
    }
  });

  return completer.future;
}