run<T> static method

PostFrameTask<T> run<T>(
  1. FutureOr<T> action(), {
  2. List<ScrollController> scrollControllers = const [],
  3. int maxWaitFrames = 5,
  4. bool waitForEndOfFrame = true,
  5. int endOfFramePasses = 2,
  6. Duration? timeout,
  7. PostFramePredicate? predicate,
  8. void onError(
    1. Object error,
    2. StackTrace stackTrace
    )?,
})

Unified advanced API returning a cancellable PostFrameTask with diagnostics encapsulated in a PostFrameResult.

If predicate is provided, it will be evaluated just before executing action. If the predicate returns false, the action is skipped and the result is marked as canceled.

If onError is provided, it will be called when an error occurs in addition to the global errorHandler if set.

Implementation

static PostFrameTask<T> run<T>(
  FutureOr<T> Function() action, {
  List<ScrollController> scrollControllers = const [],
  int maxWaitFrames = 5,
  bool waitForEndOfFrame = true,
  int endOfFramePasses = 2,
  Duration? timeout,
  PostFramePredicate? predicate,
  void Function(Object error, StackTrace stackTrace)? onError,
}) {
  final binding = WidgetsBinding.instance;
  final controllers = List<ScrollController>.unmodifiable(scrollControllers);
  final completer = Completer<PostFrameResult<T>>();
  final task = PostFrameTask<T>(completer.future);

  int endOfFramePassesWaited = 0;
  int scrollMetricWaitFrames = 0;
  int totalFramesWaited = 0;

  Timer? timeoutTimer;
  if (timeout != null) {
    timeoutTimer = Timer(timeout, () {
      if (!completer.isCompleted) {
        task.cancel();
        completer.complete(PostFrameResult<T>(
          canceled: true,
          error: TimeoutException('PostFrame.run timeout after $timeout'),
          stackTrace: StackTrace.current,
          value: null,
          endOfFramePassesWaited: endOfFramePassesWaited,
          scrollMetricWaitFrames: scrollMetricWaitFrames,
          totalFramesWaited: totalFramesWaited,
          controllers: controllers,
        ));
      }
    });
  }

  binding.addPostFrameCallback((_) async {
    try {
      if (task.isCanceled) {
        if (!completer.isCompleted) {
          completer.complete(PostFrameResult<T>(
            canceled: true,
            value: null,
            endOfFramePassesWaited: endOfFramePassesWaited,
            scrollMetricWaitFrames: scrollMetricWaitFrames,
            totalFramesWaited: totalFramesWaited,
            controllers: controllers,
          ));
        }
        return;
      }

      if (waitForEndOfFrame) {
        final passes =
            maxWaitFrames <= 0 ? 0 : endOfFramePasses.clamp(1, maxWaitFrames);
        for (var i = 0; i < passes; i++) {
          await binding.endOfFrame;
          endOfFramePassesWaited++;
          totalFramesWaited++;
          if (task.isCanceled) {
            completer.complete(PostFrameResult<T>(
              canceled: true,
              value: null,
              endOfFramePassesWaited: endOfFramePassesWaited,
              scrollMetricWaitFrames: scrollMetricWaitFrames,
              totalFramesWaited: totalFramesWaited,
              controllers: controllers,
            ));
            return;
          }
        }
      }

      if (maxWaitFrames > 0) {
        for (final controller in controllers) {
          var remaining = maxWaitFrames;
          while (!controller.hasClients && remaining-- > 0) {
            await binding.endOfFrame;
            scrollMetricWaitFrames++;
            totalFramesWaited++;
            if (task.isCanceled) {
              completer.complete(PostFrameResult<T>(
                canceled: true,
                value: null,
                endOfFramePassesWaited: endOfFramePassesWaited,
                scrollMetricWaitFrames: scrollMetricWaitFrames,
                totalFramesWaited: totalFramesWaited,
                controllers: controllers,
              ));
              return;
            }
          }
          if (!controller.hasClients) continue;
          final position = controller.position;
          var prevExtent = position.maxScrollExtent;
          var prevViewport = position.viewportDimension;
          remaining = maxWaitFrames;
          while (remaining-- > 0) {
            await binding.endOfFrame;
            scrollMetricWaitFrames++;
            totalFramesWaited++;
            if (task.isCanceled) {
              completer.complete(PostFrameResult<T>(
                canceled: true,
                value: null,
                endOfFramePassesWaited: endOfFramePassesWaited,
                scrollMetricWaitFrames: scrollMetricWaitFrames,
                totalFramesWaited: totalFramesWaited,
                controllers: controllers,
              ));
              return;
            }
            if (!controller.hasClients) break;
            if (position.maxScrollExtent != prevExtent ||
                position.viewportDimension != prevViewport) {
              break;
            }
          }
        }
      }

      if (task.isCanceled) {
        if (!completer.isCompleted) {
          completer.complete(PostFrameResult<T>(
            canceled: true,
            value: null,
            endOfFramePassesWaited: endOfFramePassesWaited,
            scrollMetricWaitFrames: scrollMetricWaitFrames,
            totalFramesWaited: totalFramesWaited,
            controllers: controllers,
          ));
        }
        return;
      }

      // Check predicate before execution.
      if (predicate != null && !predicate()) {
        if (!completer.isCompleted) {
          completer.complete(PostFrameResult<T>(
            canceled: true,
            value: null,
            endOfFramePassesWaited: endOfFramePassesWaited,
            scrollMetricWaitFrames: scrollMetricWaitFrames,
            totalFramesWaited: totalFramesWaited,
            controllers: controllers,
          ));
        }
        return;
      }

      final value = await Future.sync(action);
      if (!completer.isCompleted) {
        completer.complete(PostFrameResult<T>(
          value: value,
          canceled: false,
          endOfFramePassesWaited: endOfFramePassesWaited,
          scrollMetricWaitFrames: scrollMetricWaitFrames,
          totalFramesWaited: totalFramesWaited,
          controllers: controllers,
        ));
      }
    } catch (error, stackTrace) {
      // Invoke error handlers.
      onError?.call(error, stackTrace);
      errorHandler?.call(error, stackTrace, 'PostFrame.run');

      if (!completer.isCompleted) {
        completer.complete(PostFrameResult<T>(
          canceled: task.isCanceled,
          error: error,
          stackTrace: stackTrace,
          value: null,
          endOfFramePassesWaited: endOfFramePassesWaited,
          scrollMetricWaitFrames: scrollMetricWaitFrames,
          totalFramesWaited: totalFramesWaited,
          controllers: controllers,
        ));
      }
    } finally {
      timeoutTimer?.cancel();
    }
  });

  return task;
}