post_frame 1.1.0
post_frame: ^1.1.0 copied to clipboard
A Flutter package that provides utilities for executing actions after the current frame has been rendered, with support for waiting on scroll controllers and layout metrics.
post_frame #
post_frame is a Flutter package that provides utilities to execute actions after the first frame is rendered, making it easy to schedule work that depends on layout or widget tree completion. It supports waiting for ScrollController metrics and end-of-frame passes for precise timing of UI-dependent logic.
Version 1.1.0 introduces comprehensive post-frame scheduling features:
Core Features:
- Advanced
PostFrame.run<T>()with cancellation, timeout & diagnostics - Declarative widget builders (
PostFrame.builder,PostFrame.simpleBuilder) - Frame iteration (
PostFrame.repeat) with streams and intervals - Layout size detection (
PostFrame.onLayout) with stability checks - Task serialization (
PostFrame.queueRun) and debouncing (PostFrame.debounced)
Developer Ergonomics:
- BuildContext extensions:
context.postFrame(),context.postFrameRun(),context.postFrameDebounced() - Conditional execution with predicates and
PostFramePredicateshelpers - Global and per-call error handling (
PostFrame.errorHandler,onErrorparameter) - Rich diagnostics via
PostFrameResult<T>
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
post_frame: ^1.1.0
Features #
- Execute actions after the first frame is rendered using
PostFrame.postFrame. - Optionally wait for end-of-frame passes for more accurate layout timing.
- Wait for
ScrollControllermetrics to be available before executing actions. - Supply multiple scroll controllers to wait on complex composite layouts.
- Configure maximum wait frames to avoid indefinite waiting in edge cases.
Usage #
import 'package:post_frame/post_frame.dart';
// Example: update state after first frame
PostFrame.postFrame(() {
// Your code here, e.g. setState or navigation
});
Full Example #
import 'package:flutter/material.dart';
import 'package:post_frame/post_frame.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PostFrame Example',
home: const ExamplePage(),
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
@override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage> {
String _message = 'Waiting for post frame...';
@override
void initState() {
super.initState();
PostFrame.postFrame(() {
setState(() {
_message = 'This ran after the first frame!';
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PostFrame Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_message),
ElevatedButton(
onPressed: () {
PostFrame.postFrame(() {
setState(() {
_message = 'Button pressed after frame!';
});
});
},
child: const Text('Press Me'),
),
],
),
),
);
}
}
See example/lib/main.dart for the minimal quick start.
For comprehensive demonstrations (builders, repeat, queueRun, onLayout, predicates, debouncing, error handling, diagnostics) open example/lib/advanced_examples.dart and use PostFrameExampleApp as your app root.
Advanced Usage #
Use multiple scroll controllers and tune waiting behavior. This ensures your logic runs only after nested scrolling regions have stable metrics and several frame passes are complete.
import 'package:post_frame/post_frame.dart';
// Inside a State class
final ScrollController outerController = ScrollController();
final ScrollController innerController = ScrollController();
@override
void initState() {
super.initState();
PostFrame.postFrame(() {
// Safe to access sizes, scroll extents, etc.
debugPrint('Metrics stable: ' + outerController.position.maxScrollExtent.toString());
},
scrollControllers: [outerController, innerController],
maxWaitFrames: 10, // Give layout time to stabilize
waitForEndOfFrame: true, // Await endOfFrame signals
endOfFramePasses: 3, // Extra passes for animations/layout settling
);
}
The advanced auto-scroll merged example lives in example/lib/advanced_examples.dart and demonstrates:
- Waiting on multiple scroll controllers.
- Tuned
maxWaitFramesandendOfFramePasses. - Auto-scrolling after metrics stabilization.
Cancellation & Timeout #
Use the advanced API when you need finer control:
final task = PostFrame.run<String>(() => 'Ready',
scrollControllers: [outerController, innerController],
maxWaitFrames: 6,
endOfFramePasses: 2,
waitForEndOfFrame: true,
timeout: const Duration(seconds: 2),
);
// Optional: cancel before completion
task.cancel();
task.future.then((result) {
if (result.canceled) {
debugPrint('Action canceled');
} else if (result.hasError) {
debugPrint('Failed: ${result.error}');
} else {
debugPrint('Value: ${result.value} waited: ${result.totalFramesWaited} frames');
}
});
Builder Helpers #
Use PostFrame.builder for full control over the snapshot, or PostFrame.simpleBuilder for a ready‑made loading/data/error pattern.
// Full control builder
PostFrame.builder<int>(
scrollControllers: [verticalController],
maxWaitFrames: 4,
endOfFramePasses: 2,
action: () => verticalController.hasClients
? verticalController.position.maxScrollExtent.toInt()
: 0,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final result = snapshot.data!;
if (result.canceled) return const Text('Canceled');
if (result.hasError) return Text('Error: ${result.error}');
return Text('Extent: ${result.value}, waited ${result.totalFramesWaited} frames');
},
);
// Simple builder convenience
PostFrame.simpleBuilder<int>(
action: () => 42,
dataBuilder: (result) => Text('Value: ${result.value}'),
);
Repeat Tasks #
Run an action every frame (optionally after endOfFrame) until canceled or a maximum iteration count is reached:
final repeater = PostFrame.repeat((i) {
debugPrint('Iteration: $i');
if (i == 9) repeater.cancel(); // Stop after 10 iterations.
}, maxIterations: 10);
// Listen to iteration stream (optional):
repeater.iterationStream.listen((i) => debugPrint('Stream iteration: $i'));
// Await completion:
await repeater.done;
Parameters:
| Parameter | Use |
|---|---|
maxIterations |
Upper bound; if null repeats indefinitely until canceled. |
interval |
Inserts delay between iterations (throttling). |
waitForEndOfFrame |
If true, waits for pipeline drain each loop. |
Waiting for Layout (onLayout) #
Wait for a widget's size to become available and stable across successive frames:
final key = GlobalKey();
// In build: SizedBox(key: key, width: 200, height: 80)
final size = await PostFrame.onLayout(key, maxWaitFrames: 15);
if (size != null) {
debugPrint('Stable size: $size');
}
Parameters:
| Parameter | Purpose |
|---|---|
maxWaitFrames |
Upper bound on frame polls. |
stabilityFrames |
Required consecutive identical sizes. |
waitForEndOfFrame |
Use endOfFrame for stronger layout finality. |
Queued Post-frame Runs #
Ensure multiple advanced tasks execute sequentially:
final first = PostFrame.queueRun(() => 'A');
final second = PostFrame.queueRun(() => 'B');
final a = await first.future; // Completes before second starts
final b = await second.future;
debugPrint('${a.value}, ${b.value}'); // A, B
You can clear not-yet-started tasks:
PostFrame.clearQueue();
BuildContext Extensions #
Use convenient extensions for ergonomic widget-context access with automatic mounted checks:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Simple post-frame with mounted check
context.postFrame(() {
// Safe: automatically checks if widget is mounted
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Frame complete')),
);
});
// Advanced with cancellation and diagnostics
context.postFrameRun<int>(() {
return 42;
}, timeout: const Duration(seconds: 1));
// Debounced (cancels previous tasks with same key)
context.postFrameDebounced(() {
// Only latest call executes
}, debounceKey: 'myKey');
return Container();
}
}
Conditional Execution with Predicates #
Control when actions execute using predicates:
// Only execute if mounted
PostFrame.run(() {
// action
}, predicate: PostFramePredicates.mounted(context));
// Only if route is still active
PostFrame.run(() {
// action
}, predicate: PostFramePredicates.routeActive(context));
// Only if scroll extent meets threshold
PostFrame.run(() {
// action
}, predicate: PostFramePredicates.scrollExtentAtLeast(controller, 100.0));
// Combine predicates with AND/OR/NOT
PostFrame.run(() {
// action
}, predicate: PostFramePredicates.all([
PostFramePredicates.mounted(context),
PostFramePredicates.routeActive(context),
]));
Available predicates:
PostFramePredicates.mounted(context)- Widget is still mountedPostFramePredicates.stateMounted(state)- State is still mountedPostFramePredicates.routeActive(context)- Current route is activePostFramePredicates.scrollControllerHasClients(controller)- Controller has clientsPostFramePredicates.scrollExtentAtLeast(controller, minExtent)- Scroll extent thresholdPostFramePredicates.all([...])- Combine with AND logicPostFramePredicates.any([...])- Combine with OR logicPostFramePredicates.not(predicate)- Negate predicate
Debounced Actions #
Prevent redundant work by canceling previous pending tasks:
// Rapid successive calls - only last executes
for (var i = 0; i < 100; i++) {
PostFrame.debounced(() {
debugPrint('Only once!');
}, debounceKey: 'my-operation');
}
// Different keys don't interfere
PostFrame.debounced(() => updateUI(), debounceKey: 'ui');
PostFrame.debounced(() => saveData(), debounceKey: 'save');
Error Handling #
Set up global error handling for debugging:
void main() {
// Global handler for all PostFrame errors
PostFrame.errorHandler = (error, stack, operation) {
debugPrint('PostFrame error in $operation: $error');
// Log to analytics, crash reporting, etc.
};
runApp(MyApp());
}
// Per-call error handler
PostFrame.run(() {
throw Exception('Oops');
}, onError: (error, stack) {
// Handle specific error
debugPrint('Task failed: $error');
});
Diagnostics (PostFrameResult) #
Fields available:
value- The returned value from the callback (if successful).canceled- Whether the task was canceled before completion.error/stackTrace- Error information if callback threw or timeout occurred.endOfFramePassesWaited- Number of end-of-frame passes actually waited.scrollMetricWaitFrames- Number of frame polls spent waiting on scroll metrics.totalFramesWaited- Sum of all frame waits (passes + metric polls).controllers- The controllers targeted for stabilization.
Parameter Reference #
| Parameter | Purpose | Recommended Use |
|---|---|---|
scrollControllers |
Ensures each controller has clients and stable metrics before running action. | Pass all controllers you depend on (e.g., nested ListView, PageView). |
maxWaitFrames |
Upper bound on how many frames to wait for metrics stabilization. | Increase for complex layouts; default 5 covers most simple screens. |
waitForEndOfFrame |
If true, waits for binding.endOfFrame before running action. |
Keep true when syncing with final layout/paint. |
endOfFramePasses |
Additional end-of-frame passes to allow animations/rebuilds to settle. | Use 2-4 when lists populate asynchronously. |
Best Practices #
- Keep
maxWaitFramesmodest to avoid excessive delays. - Consider wrapping
PostFrame.postFrameinside your own utility for repeated patterns. - Avoid heavy synchronous work in the callback; schedule microtasks or isolates if needed.
- If you need cancellation, consider a wrapper Future that you can ignore (feature planned).
License #
This project is licensed under the MIT License. See the LICENSE file for details.