Skip to content

Using BuildContext Safely in Asynchronous Code

Problem

When working with Flutter, you encounter a common lint warning: "Do not use BuildContexts across async gaps." This occurs when you try to use a BuildContext after an asynchronous operation, but the widget may no longer be in the widget tree.

The core issue is that BuildContext becomes invalid if the widget is removed from the tree during the async operation, leading to potential crashes when trying to access navigation methods or other context-dependent functionality.

Why This Matters

Using a stale BuildContext can cause:

  • App crashes with "setState() called after dispose()" errors
  • Navigation failures when trying to push/pop routes
  • Memory leaks from holding references to disposed widgets
  • Unexpected behavior in UI updates

Solutions

Since Flutter 3.7, BuildContext includes a mounted property that works in both StatefulWidget and StatelessWidget:

dart
Future<void> myAsyncMethod(BuildContext context) async {
  await someFuture();
  
  if (context.mounted) {
    Navigator.of(context).pop();
  }
}

WARNING

In StatefulWidgets, you can use the mounted property directly from the state class, but prefer context.mounted for consistency across both widget types.

2. Pass Context as Parameter (Best Practice)

Avoid storing BuildContext in custom classes. Instead, pass it as a parameter when needed:

dart
class MyCustomClass {
  Future<void> myAsyncMethod(BuildContext context) async {
    await Future.delayed(const Duration(seconds: 2));
    
    if (context.mounted) {
      Navigator.of(context).pop();
    }
  }
}

// Usage in widget
IconButton(
  onPressed: () => MyCustomClass().myAsyncMethod(context),
  icon: const Icon(Icons.action),
);

3. Extract Navigation Objects Before Async Operations

Capture navigation-related objects before the async gap:

dart
void myAsyncMethod() async {
  final navigator = Navigator.of(context);
  await someFuture();
  
  navigator.pop(); // Safe to use
}

4. Use Callbacks for Complex Scenarios

For more complex scenarios, use callbacks to handle post-async operations:

dart
class MyCustomClass {
  Future<void> myAsyncMethod(VoidCallback onSuccess) async {
    await Future.delayed(const Duration(seconds: 2));
    onSuccess.call();
  }
}

// In your widget
MyCustomClass().myAsyncMethod(() {
  if (mounted) {
    Navigator.of(context).pop();
  }
});

Common Pitfalls to Avoid

DANGER

Never disable the lint rule instead of fixing the underlying issue:

yaml
# DON'T disable this warning
linter:
  rules:
    use_build_context_synchronously: false

WARNING

Avoid storing BuildContext in class fields or global variables. This creates hard-to-debug issues when the context becomes stale.

When to Use Which Approach

ApproachBest ForConsiderations
context.mountedSimple async operationsWorks in all widget types
Passing context as parameterCustom classesPrevents storing stale context
Extracting navigation objectsNavigation-focused codeLimited to navigation operations
CallbacksComplex async workflowsRequires careful state management

Complete Example

Here's a complete implementation showing the recommended approach:

dart
class ApiService {
  Future<void> fetchDataAndNavigate(
    BuildContext context, 
    String endpoint
  ) async {
    // Show loading dialog
    showDialog(
      context: context,
      builder: (context) => const CircularProgressIndicator(),
    );
    
    // Simulate API call
    await Future.delayed(const Duration(seconds: 2));
    
    // Check if context is still valid
    if (context.mounted) {
      Navigator.of(context).pop(); // Close dialog
      Navigator.of(context).push(
        MaterialPageRoute(builder: (context) => ResultsPage()),
      );
    }
  }
}
dart
class MyWidget extends StatelessWidget {
  final ApiService apiService = ApiService();
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => apiService.fetchDataAndNavigate(context, '/data'),
      child: const Text('Fetch Data'),
    );
  }
}

Key Takeaways

  1. Always check context.mounted before using BuildContext after async operations
  2. Never store BuildContext in class fields - pass it as a parameter instead
  3. The lint rule exists to prevent hard-to-debug crashes
  4. Flutter 3.7+ provides context.mounted for both Stateful and Stateless widgets
  5. Prefer solutions that keep context usage localized and predictable

By following these patterns, you can safely work with asynchronous operations while avoiding common pitfalls with BuildContext usage.