← Back to posts Cover image for Fixing Flutter ANR Issues: Strategies for Unblocking the Main Thread

Fixing Flutter ANR Issues: Strategies for Unblocking the Main Thread

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Is your Flutter app freezing, showing the dreaded “App isn’t responding” dialog, or just feeling unresponsive? You might be dealing with an Application Not Responding (ANR) error. A common misconception is to write these off as general performance issues—“the app is just slow.” In reality, ANRs are a specific and critical failure: your app’s main thread (also called the UI thread) has been completely blocked for too long, preventing it from processing user input or rendering frames.

The main thread in Flutter is a single-threaded event loop. It’s responsible for everything you see and interact with: drawing widgets, handling taps, running animations, and executing your build methods. If you perform a heavy, synchronous operation on this thread, the entire UI freezes until that task is complete. The operating system watches for this. If your app fails to respond to input events (like a tap) within a certain window (typically 5 seconds on Android), it triggers the ANR dialog.

The key to fixing ANRs is shifting your mindset: stop looking for generic “slowness” and start hunting for blocking operations on the main thread.

Common Culprits That Block Your UI

Let’s look at some typical offenders:

  1. Synchronous I/O Operations: Reading/writing large files, making synchronous network calls, or performing heavy database queries on the main thread.
  2. Complex Synchronous Computations: Image processing, complex list filtering/sorting, JSON parsing of large data, or any CPU-intensive task done synchronously.
  3. Dart’s compute Function Gone Wrong: While compute is the go-to for moving work to an isolate, passing excessively large arguments to or from it can cause blocking serialization/deserialization on the main thread.
  4. Poorly Managed Stream/Future Chains: Using .then or callbacks that ultimately run expensive code on the main thread because you didn’t specify where the work should happen.

Strategies and Practical Fixes

Here’s how you can identify and resolve these blockers.

1. Offload Work with compute and Isolates

For pure, heavy Dart computations, use compute. It spawns a separate isolate, keeping the main thread free.

The Problem (Blocking UI):

void _processUserData() {
  // This blocks the UI thread for a long time!
  List<ComplexUser> filteredUsers = allUsers.where((user) {
    // Simulating a complex evaluation
    return someExpensiveValidation(user.profileData);
  }).toList();
  setState(() {
    _users = filteredUsers;
  });
}

The Solution (Using compute):

// This function MUST be top-level or static to be sent to another isolate.
static List<ComplexUser> _filterUsersInBackground(List<ComplexUser> allUsers) {
  return allUsers.where((user) {
    return someExpensiveValidation(user.profileData);
  }).toList();
}

Future<void> _processUserData() async {
  // This returns immediately, the UI stays responsive.
  List<ComplexUser> filteredUsers = await compute(_filterUsersInBackground, allUsers);
  setState(() {
    _users = filteredUsers;
  });
}

2. Use Asynchronous I/O Correctly

The dart:io and dart:convert libraries provide asynchronous versions of most methods. Use them! Remember: async doesn’t automatically mean non-blocking; you must use the async APIs.

The Problem (Blocking File Read):

String _loadAppConfig() {
  // BAD: Synchronous file read.
  File configFile = File('path/to/config.json');
  return configFile.readAsStringSync(); // UI waits here.
}

The Solution (Async File Read):

Future<void> _loadAppConfig() async {
  File configFile = File('path/to/config.json');
  // GOOD: Asynchronous read. The main thread can process other events while waiting for the OS.
  String contents = await configFile.readAsString();
  // Parse the JSON AFTER it's loaded, but parsing could also be heavy...
  Map<String, dynamic> config = jsonDecode(contents);
  // For large JSON, consider moving decode to compute as well.
  _updateConfig(config);
}

3. Profile to Find the Hidden Block

Use the Dart DevTools Performance view and CPU Profiler. Record a performance session while performing the action that leads to the hang.

  • Look for long, solid bars on the main thread (UI) track.
  • Click on those bars to see the exact method that was running. You’ll often find your culprit right there—a parsing function, a file operation, or a dense loop.

4. Be Smart with State Management and Builds

While less likely to cause a full 5-second ANR, excessive work in build() methods or state change notifications can make the app feel frozen and contribute to jank.

  • Break up large widget trees with const constructors and AutomaticKeepAliveClientMixin.
  • Use ListView.builder for lists; it’s non-blocking.
  • Throttle state updates from streams if they fire too rapidly.

Key Takeaway

Fixing ANRs isn’t about micro-optimizing every line of code. It’s about auditing your app for operations that have no business being on the main thread and moving them off. Treat the main thread with respect: its primary jobs are handling input and painting frames. Anything else that takes more than a few milliseconds should be a candidate for asynchronous execution or offloading to an isolate.

Start by profiling your app during its slowest operations. The profiler will visually show you the blocking calls. Then, systematically apply the strategies above—use async/await with truly async APIs, employ compute for heavy lifting, and structure your app to keep the main thread as idle as possible, ready to respond to the user instantly.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.