Mobile 8 min read

Scaling Flutter in Production: Achieving 1% Crash Rates Across Millions of Sessions

How we reengineered government mobile apps into a single Flutter codebase with modular state management, automated error boundaries, and baseline coding standards.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira

When managing institutional mobile applications serving tens of thousands of active personnel across government ministries, stability is not optional. App crashes directly interrupt official document processing, time-sensitive approvals, and critical notifications.

In this article, we share practical architectural patterns from reengineering the E-Kemenkeu mobile ecosystem using Flutter, consolidating separate native iOS and Android codebases into a single resilient codebase while reducing crash rates to just 1%.

Why We Unified the Codebase with Flutter

Prior to reengineering, maintaining separate Swift/Objective-C and Java/Kotlin applications resulted in:

  1. Asynchronous Release Cycles: Features launched on Android weeks before iOS.
  2. Divergent Bug Profiles: OS-specific quirks required duplicate debugging efforts.
  3. High Overhead: Two engineering streams maintaining identical business rules.

Flutter provided high performance via Skia/Impeller rendering, expressive UI flexibility, and true cross-platform parity.

Core Architectural Pillars for 1% Crash Rate

1. Global Error Boundaries & Unhandled Exception Trapping

Unhandled asynchronous exceptions are the primary source of application crashes. We wrapped the Flutter root execution with structured zone error handling:

void main() {
  runZonedGuarded<Future<void>>(() async {
    WidgetsFlutterBinding.ensureInitialized();
    
    // Set custom Flutter error callback
    FlutterError.onError = (FlutterErrorDetails details) {
      FlutterError.presentError(details);
      CrashAnalyticsService.recordError(details.exception, details.stack);
    };

    runApp(const EKemenkeuApp());
  }, (error, stack) {
    CrashAnalyticsService.recordError(error, stack);
  });
}

[!WARNING] Never allow asynchronous unhandled Future errors to bubble up past the zone root. Always implement explicit try/catch handlers inside background worker isolates.

2. Predictable State Management & Immutability

To avoid memory leaks and inconsistent render states, we adopted unidirectional state flows. UI components never mutate business state directly; instead, they dispatch events and react to immutable state emissions.

@immutable
abstract class DocumentState {}

class DocumentLoading extends DocumentState {}

class DocumentLoaded extends DocumentState {
  final List<OfficialDocument> documents;
  final bool hasReachedMax;

  DocumentLoaded({required this.documents, this.hasReachedMax = false});
}

3. Baseline Coding Standards & Team Linting

We enforced strict analysis options across the team:

  • Disallowed implicit dynamic types.
  • Required const constructors for immutable widget rebuild optimizations.
  • Mandatory automated widget and unit test coverage before pull request merge.

Conclusion

By treating error handling as a first-class architectural concern, enforcing strict typing, and consolidating to Flutter, enterprise mobile applications can achieve supreme stability without sacrificing feature velocity.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.