Mixpanel, Firebase, and Multi-Analytics setup in Flutter

How to send analytics events to multiple providers from a single source of truth

Mixpanel, Firebase, and Multi-Analytics setup in Flutter

How to send analytics events to multiple providers from a single source of truth

If you often build something, you create some tools along the way. You design some blueprints that come in handy when you want to do something similar. This is exactly that.

For reading this article without a membership

Tracking user behavior is essential to understand how people actually use your app — not just how you think they do.

But when it comes to choosing an analytics tool, you’re often stuck with a trade-off: Firebase is great for out-of-the-box dashboards and Crashlytics, while Mixpanel gives you deep funnel analysis, retention breakdowns, and custom event properties. You might also have your custom-built event-tracking system in place.

So why not use all?

In this post, I’ll show you how to set up multi-provider analytics in Flutter. We’ll connect Firebase Analytics and Mixpanel, and build a shared interface so that you can log an event once and send it to as many services as you want behind the scenes.

Less boilerplate, more insight.

But why Multi-Analytics?

  • You don’t want vendor lock-in.
  • Some tools are better at different things.
  • Product, marketing, and dev teams often need different types of data.
  • You want flexibility without adding tech debt.

Rather than scattering logEvent calls everywhere, we’ll create an abstraction layer that cleanly sends data to multiple destinations.

Content

  1. Building a safe and abstract Analytics Provider
  2. Client-specific implementations
  3. A manager who manages all the implementations
  4. A singleton for accessibility over the project

Building a safe and abstract Analytics Provider

Source

Let’s suppose you are building multiple houses. Think of your AnalyticsProvider as a blueprint — not for one house, but for how to build any house.

You’re the builder. And in your toolkit, you’ve got a standard design:

• Every house must have doors (logIn)

• Windows (logOut)

• A roof (logEvent)

That’s your abstract class — it defines what every analytics provider must include, no matter how it’s built or what tools it uses.

abstract class AnalyticsProvider { 
  Future<void> logEvent(String name, {Map<String, dynamic>? params}); 
  Future<void> logIn(String userId, {Map<String, dynamic>? params}); 
  Future<void> logOut(); 
}

This AnalyticsProvider class is an abstract interface — a contract that defines the structure any analytics implementation must follow in your app. The AnalyticsProvider class defines a common structure for analytics services (like Firebase, Mixpanel, or others). It ensures that every analytics backend you plug in behaves consistently and supports the same basic methods.

Instead of directly writing FirebaseAnalytics.logEvent(…) or Mixpanel.track(…) everywhere, you create provider classes that implement this interface, and then call the shared methods generically.

Before we move further, we must create an Event class and a Key class so that we don’t have the key and event names scattered throughout the app.

part 'analytics_keys.dart'; 
 
class AnalyticsConstants { 
  /// onboarding 
  static const String onboardingStartedEvent = 'User Onboarding Started'; 
  ... 
}
part of 'analytics_constants.dart'; 
 
class AnalyticsKeys { 
  // user 
  static const String age = 'age'; 
  ... 
}

Client-specific implementations

Now, say you’re building in different cities:

• Firebase is your Mumbai house — maybe it’s minimal, but integrated with the neighbourhood.

• Mixpanel is your Berlin house — more tracking rooms, and maybe some fancy behaviour sensors.

• Tomorrow, someone asks for Amplitude or Segment — you don’t panic. You’ve got the blueprint. You just build a new house using the same foundation.

The beauty?

All your clients (your app code) only need to know the blueprint — they don’t care which house they’re in.

They just say: “Hey, record this event” — and behind the scenes, all the right houses light up.

So let’s build your Firebase Analytics Provider. Just implement the Analytics Provider and voila!

import 'dart:convert'; 
 
import 'package:firebase_analytics/firebase_analytics.dart'; 
import 'package:app/analytics/analytics_provider.dart'; 
import 'package:app/user_utils.dart'; 
 
class FirebaseAnalyticsProvider implements AnalyticsProvider { 
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; 
   
  @override 
  Future<void> logEvent(String name, {Map<String, dynamic>? params}) { 
    try { 
      final parameters = <String, Object>{}; 
 
      // add parameters 
      if (params != null && params.isNotEmpty) { 
        // filter out null values and convert remaining to Objects 
        // since firebase ananlytics does not allow null objects 
        params.forEach((key, value) { 
          if (value != null) { 
            parameters.addAll({ 
              key: value 
            }); 
          } 
        }); 
      } 
      // Prepare the timestamp and add 
      String timeStamp = DateTime.now().toString(); 
      parameters.addAll({ 
        'timestamp': timeStamp, 
      }); 
 
      // add user id 
      parameters.addAll({ 
        'user_id': UserUtils.userId, 
      }); 
 
      return _analytics.logEvent( 
          name: name, parameters: parameters); 
    } catch (ex) { 
      // failure to log event 
      return Future.value(); 
    } 
  } 
 
  @override 
  Future<void> logIn(String userId, {Map<String, dynamic>? params}) { 
    _analytics.setUserId(id: userId); 
    if (params != null) { 
      params.forEach((key, value) { 
        _analytics.setUserProperty(name: key, value: value); 
      }); 
    } 
    return Future.value(); 
  } 
 
  @override 
  Future<void> logOut() { 
    // do nothing, firebase log out will handle it 
    return Future.value(); 
  } 
}

You can add a MixPanel Provider as well:

import 'dart:convert'; 
 
import 'package:mixpanel_flutter/mixpanel_flutter.dart'; 
import 'package:app/analytics/analytics_provider.dart'; 
import 'package:app/user_utils.dart'; 
 
class MixpanelAnalyticsProvider implements AnalyticsProvider { 
  final Mixpanel analytics; 
 
  MixpanelAnalyticsProvider(this.analytics); 
 
  @override 
  Future<void> logEvent(String name, {Map<String, dynamic>? params}) { 
    try { 
      final parameters = <String, dynamic>{}; 
 
      // add parameters 
      if (params != null && params.isNotEmpty) { 
        parameters.addAll(params); 
      } 
      // Prepare the timestamp and add 
      String timeStamp = DateTime.now().toString(); 
      parameters.addAll({ 
        'timestamp': timeStamp, 
      }); 
 
      // add user id 
      parameters.addAll({ 
        'user_id': UserUtils.userId, 
      }); 
 
      return analytics.track(name, properties: parameters); 
    } catch (ex) { 
      // failure to log event 
      return Future.value(); 
    } 
  } 
 
  @override 
  Future<void> logIn(String userId, {Map<String, dynamic>? params}) { 
    analytics.identify(userId); 
    if (params != null && params.isNotEmpty) { 
      params.forEach((key, value) { 
        analytics.getPeople().set(key, value); 
      }); 
    } 
    return Future.value(); 
  } 
 
  @override 
  Future<void> logOut() { 
    return analytics.reset(); 
  } 
}

And you can add any of your custom providers as well.

A manager who manages all the implementations

Source

If the AnalyticsProvider is your blueprint, then the AnalyticsManager is the general contractor.

You, the app, don’t go talk to each builder (Mixpanel, Firebase) separately. You talk to the contractor, and he makes sure every house gets built the right way.

Each provider in _providers is a different house being built:

  • Firebase house
  • Mixpanel house
  • Amplitude house (maybe one day)

When you call:

Analytics.instance.logEvent(AnalyticsKeys.productViewed, params: {...});

…it’s like saying:

“All houses need a skylight in the kitchen.”

And then the contractor (your AnalyticsManager) walks to each builder with the same instruction:

• Firebase adds a skylight

• Mixpanel adds a skylight

• Any other analytics house adds a skylight

You only made one request, but multiple houses got updated.

import 'package:app/analytics/analytics_provider.dart'; 
 
class AnalyticsManager { 
  final List<AnalyticsProvider> _providers; 
 
  AnalyticsManager(this._providers); 
 
  Future<void> logEvent(String name, {Map<String, dynamic>? params, Map<String, dynamic>? eventData}) async { 
    for (var provider in _providers) { 
     await provider.logEvent(name, params: params, eventData: eventData); 
    } 
  } 
 
  Future<void> login(String userId, {Map<String, dynamic>? params}) async { 
    for (var provider in _providers) { 
      await provider.logIn(userId, params: params); 
    } 
  } 
 
  Future<void> logOut() async { 
    for (var provider in _providers) { 
      await provider.logOut(); 
    } 
  } 
}

A singleton for accessibility over the project

Source

This file is the construction site manager’s office. It’s where the crew gets assembled, blueprints are loaded, and the real building starts.

This is the Analytics HQ. It sets up:

  • The contractor (AnalyticsManager)
  • The houses (Firebase, Mixpanel, Custom tracking)
  • The configuration (like choosing what features get built)
import 'dart:convert'; 
 
import 'package:flutter/foundation.dart'; 
import 'package:mixpanel_flutter/mixpanel_flutter.dart'; 
import 'package:app/analytics/analyics_manager.dart'; 
import 'package:app/analytics/custom_analytics_provider.dart'; 
import 'package:app/analytics/mixpanel_analytics_provider.dart'; 
import 'package:app/service/config/firebase_remote_config_service.dart'; 
 
class Analytics { 
  static late AnalyticsManager _instance; 
 
  static Future<void> init() async { 
    const mixpanelKey = kDebugMode 
        ? 'debugKey' 
        : 'prodKey'; 
 
    final mixpanel = await Mixpanel.init( 
      mixpanelKey, 
      trackAutomaticEvents: true, 
    ); 
    _instance = AnalyticsManager([ 
      CustomAnalyticsProvider( 
        acceptedEvents: _acceptedEvents, 
      ), 
      MixpanelAnalyticsProvider(mixpanel) 
    ]); 
  } 
 
  static AnalyticsManager get instance => _instance; 
 
  // This method fetches the accepted events from the remote config 
  // which are to be pushed to local analytics  
  static List<String> get _acceptedEvents { 
    final events = <String>[]; 
    ... 
    return events; 
  } 
}

Now, you can initialise the Analytics in main.dart and use it like this:

Future<void> main() async { 
  runZonedGuarded(() async { 
    WidgetsFlutterBinding.ensureInitialized(); 
 
    await Firebase.initializeApp( 
      options: DefaultFirebaseOptions.currentPlatform, 
    ); 
 
    FlutterError.onError = (errorDetails) { 
      FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); 
    }; 
    // Pass all uncaught asynchronous 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; 
    }; 
    await FirebaseRemoteConfigService.instance.initialize(); 
    await Analytics.init(); 
 
    runApp(MyApp( 
      themeCode: prefs.getString('theme_code'), 
    )); 
  }, (error, stacktrace) { 
    debugPrint("Error $error $stacktrace"); 
  }); 
}
Analytics.instance.logEvent(AnalyticsKeys.productViewed, params: {...});

Phew. You made it. You read the whole thing. That’s wild.

Thanks for sticking around — either you’re super curious or accidentally left your screen on while making coffee. Either way, I appreciate you.

Now, if this post helped, entertained, or mildly improved your day, feel free to smash that clap button. Medium lets you clap up to 50 times, which is… a weirdly high number. But hey, go wild. Pretend you’re giving a round of applause with both hands, twice.

No pressure, of course. But also… pressure.

Thanks again. You’re the best.

Thank you for being a part of the community

Before you go: