Integrating with Crashlytics in Flutter

Ensuring the stability and reliability of your Flutter applications is paramount. Integrating crash reporting tools, such as Firebase Crashlytics, allows developers to capture, analyze, and address crashes and errors effectively. This guide provides a comprehensive walkthrough of integrating Firebase Crashlytics into your Flutter application, along with best practices for robust error reporting.

What is Firebase Crashlytics?

Firebase Crashlytics is a real-time crash reporting tool that helps you track, prioritize, and fix stability issues in your apps. Crashlytics provides detailed reports of your app’s crashes, including stack traces, device information, and custom logs, enabling faster debugging and improved app quality.

Why Integrate Crashlytics in Flutter?

  • Real-Time Crash Reporting: Immediate notifications when your app crashes.
  • Detailed Crash Analysis: Comprehensive crash reports including stack traces and device info.
  • Prioritization of Issues: Ability to prioritize and address the most critical crashes.
  • Customizable Reporting: Capability to log custom events and user information to aid in debugging.

Prerequisites

Before integrating Crashlytics, ensure you have:

  • A Firebase project set up.
  • A Flutter project.
  • FlutterFire CLI installed and configured.

Step-by-Step Integration Guide

Step 1: Set Up Firebase Project

If you haven’t already, create a new Firebase project in the Firebase Console:
Firebase Console.

Follow these steps:

  1. Go to the Firebase Console and click “Add project”.
  2. Enter your project name and follow the setup instructions.
  3. Once the project is created, add a new app by selecting either iOS or Android, based on your Flutter project’s targets.

Step 2: Add Firebase to Your Flutter Project

Use FlutterFire CLI to connect your Flutter project to your Firebase project. FlutterFire simplifies the configuration process by automatically updating necessary files and configurations.

Open your Flutter project directory in the terminal and run:

flutterfire configure

This command guides you through selecting your Firebase project and configuring Firebase options for both iOS and Android.

Step 3: Install Required Flutter Packages

Add the necessary Firebase packages to your Flutter project’s pubspec.yaml file:

dependencies:
  firebase_core: ^2.15.0
  firebase_crashlytics: ^3.3.5

Then, run flutter pub get to install the packages.

Step 4: Initialize Firebase

Initialize Firebase in your Flutter application’s main function. Ensure you handle potential errors during initialization.

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Import generated Firebase options

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };
  // Pass all uncaught synchronous errors that aren't handled by the Flutter framework to Crashlytics
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Crashlytics Example',
      home: MyHomePage(),
    );
  }
}

Step 5: Implement Crash Reporting

Now, you can start using Crashlytics in your Flutter application. There are several ways to report errors and crashes.

Catching and Reporting Errors

You can catch specific errors and report them to Crashlytics using try-catch blocks.

try {
  // Code that might throw an exception
  throw Exception('This is a test exception');
} catch (e, stack) {
  FirebaseCrashlytics.instance.recordError(e, stack, reason: 'Test exception');
}
Logging Custom Events and Messages

Log custom events and messages to get more context around crashes. This is especially helpful for debugging user-specific issues.

FirebaseCrashlytics.instance.log('User performed action X');
Simulating a Crash

You can force a crash to test if Crashlytics is working correctly.

ElevatedButton(
  onPressed: () {
    FirebaseCrashlytics.instance.crash();
  },
  child: Text('Simulate Crash'),
)
Set User Identifier

Associating crash reports with user identifiers can help track issues related to specific users.

FirebaseCrashlytics.instance.setUserIdentifier('user123');
Setting Custom Keys

Use custom keys to add more contextual information to your crash reports.

FirebaseCrashlytics.instance.setCustomKey('environment', 'production');
FirebaseCrashlytics.instance.setCustomKey('version', '1.0.0');

Step 6: Handling Uncaught Exceptions

To ensure that all uncaught exceptions are reported, you can override FlutterError.onError and PlatformDispatcher.instance.onError to record fatal errors.

import 'dart:ui';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Import generated Firebase options

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };
  // Pass all uncaught synchronous errors that aren't handled by the Flutter framework to Crashlytics
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Crashlytics Example',
      home: MyHomePage(),
    );
  }
}

Best Practices for Effective Crash Reporting

  • Initialize Crashlytics Early: Initialize Crashlytics as early as possible in your app’s lifecycle to catch early crashes.
  • Use Descriptive Logs: Add meaningful logs that provide context about user actions and app state before a crash.
  • Set User Identifiers: Associate crash reports with user IDs to better understand user-specific issues.
  • Test Your Integration: Simulate crashes and errors to ensure Crashlytics is capturing reports correctly.
  • Monitor Crash Reports Regularly: Regularly check the Firebase Console for new crash reports and prioritize them based on severity.
  • Implement Error Boundaries: Wrap sections of your app with error boundaries to catch and handle errors gracefully, preventing the entire app from crashing.

Example Code

Here’s a complete example integrating Crashlytics with a simple Flutter app:

import 'dart:ui';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Import generated Firebase options

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };
  // Pass all uncaught synchronous errors that aren't handled by the Flutter framework to Crashlytics
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Crashlytics Example',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Crashlytics Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                try {
                  // Simulate an exception
                  throw Exception('Test exception');
                } catch (e, stack) {
                  FirebaseCrashlytics.instance.recordError(e, stack, reason: 'Test exception');
                }
              },
              child: Text('Throw Test Exception'),
            ),
            ElevatedButton(
              onPressed: () {
                FirebaseCrashlytics.instance.log('Button pressed to trigger crash');
                FirebaseCrashlytics.instance.crash();
              },
              child: Text('Simulate Crash'),
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

Integrating Firebase Crashlytics into your Flutter application provides invaluable insights into app stability and crash patterns. By following these steps and implementing best practices, you can effectively monitor, analyze, and address crashes, leading to a more stable and reliable user experience. Properly configured crash reporting is an essential part of any robust Flutter development workflow, enabling faster debugging, proactive issue resolution, and continuous app improvement.